{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "trigger-1",
      "name": "Google Drive Trigger",
      "type": "n8n-nodes-base.googleDriveTrigger",
      "position": [
        0,
        -160
      ],
      "parameters": {
        "event": "fileCreated",
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "specificFolder",
        "folderToWatch": {
          "__rl": true,
          "mode": "list",
          "value": "1vleObg6ZBzUflpAVT2Joc5D7oBNYWNUw",
          "cachedResultUrl": "https://drive.google.com/drive/folders/1vleObg6ZBzUflpAVT2Joc5D7oBNYWNUw",
          "cachedResultName": "Lendium Files"
        }
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "code-prepare",
      "name": "Prepare File Info",
      "type": "n8n-nodes-base.code",
      "position": [
        224,
        -160
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Extract and normalize file metadata from Google Drive Trigger\nconst triggerData = $input.item.json;\n\nconst fileId = triggerData.id || triggerData.fileId || '';\nconst fileName = triggerData.name || triggerData.title || 'unknown';\nconst mimeType = triggerData.mimeType || triggerData.mime_type || '';\nconst fileSize = triggerData.size || triggerData.fileSize || 0;\nconst createdTime = triggerData.createdTime || triggerData.createdDate || new Date().toISOString();\n\nreturn {\n  json: {\n    fileId,\n    fileName,\n    mimeType,\n    fileSize: parseInt(fileSize, 10) || 0,\n    createdTime,\n    originalData: triggerData\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "gdrive-download",
      "name": "Download File",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        448,
        -160
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.fileId }}"
        },
        "options": {},
        "operation": "download"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "if-validate-binary",
      "name": "Validate Binary",
      "type": "n8n-nodes-base.if",
      "position": [
        672,
        -160
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "binary-check",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ !!$binary && !!$binary.data }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "crypto-hash",
      "name": "Crypto",
      "type": "n8n-nodes-base.crypto",
      "position": [
        896,
        -256
      ],
      "parameters": {
        "binaryData": true,
        "dataPropertyName": "hash"
      },
      "typeVersion": 1
    },
    {
      "id": "supa-check-hash",
      "name": "Check Hash Exists",
      "type": "n8n-nodes-base.supabase",
      "position": [
        1120,
        -256
      ],
      "parameters": {
        "limit": 1,
        "filters": {
          "conditions": [
            {
              "keyName": "hash",
              "keyValue": "={{ $json.hash }}",
              "condition": "eq"
            }
          ]
        },
        "tableId": "file_hashes",
        "matchType": "allFilters",
        "operation": "getAll"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "gdrive-move-dup",
      "name": "Move to Duplicates",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        1792,
        -256
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Prepare File Info').item.json.fileId }}"
        },
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive",
          "cachedResultUrl": "https://drive.google.com/drive/my-drive",
          "cachedResultName": "My Drive"
        },
        "folderId": {
          "__rl": true,
          "mode": "list",
          "value": "17_l9LfnpyJas6iZy3sIMNNdfi4W7ia6o",
          "cachedResultUrl": "https://drive.google.com/drive/folders/17_l9LfnpyJas6iZy3sIMNNdfi4W7ia6o",
          "cachedResultName": "Duplicates"
        },
        "operation": "move"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "slack-notify",
      "name": "Notify Duplicate",
      "type": "n8n-nodes-base.slack",
      "position": [
        2016,
        -256
      ],
      "parameters": {
        "text": "=\ud83d\udd34 *Duplicate File Detected*\n\n\ud83d\udcc4 *File:* {{ $('Prepare File Info').item.json.fileName }}\n\ud83c\udd94 *File ID:* {{ $('Prepare File Info').item.json.fileId }}\n\ud83d\udd11 *MD5 Hash:* {{ $('Crypto').item.json.hash }}\n\ud83d\udce6 *Size:* {{ $('Prepare File Info').item.json.fileSize }} bytes\n\ud83d\udcc2 *Action:* Moved to Duplicates folder\n\u23f0 *Time:* {{ $now.toISO() }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0ABU5WAKLH",
          "cachedResultName": "estateline-ai"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.4
    },
    {
      "id": "code-prep-dup-log",
      "name": "Prepare Dup Log Data",
      "type": "n8n-nodes-base.code",
      "position": [
        2464,
        -256
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Prepare data for Supabase insert into dedup_audit_log\nreturn {\n  json: {\n    file_id: $('Prepare File Info').item.json.fileId,\n    file_name: $('Prepare File Info').item.json.fileName,\n    hash: $('Crypto').item.json.hash,\n    status: 'duplicate',\n    action_taken: 'moved_to_duplicates'\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "supa-log-dup",
      "name": "Log Duplicate Event",
      "type": "n8n-nodes-base.supabase",
      "position": [
        2688,
        -256
      ],
      "parameters": {
        "tableId": "dedup_audit_log",
        "dataToSend": "autoMapInputData"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "code-prep-insert-hash",
      "name": "Prepare Hash Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1792,
        96
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Prepare data for Supabase insert into file_hashes\nreturn {\n  json: {\n    file_id: $('Prepare File Info').item.json.fileId,\n    file_name: $('Prepare File Info').item.json.fileName,\n    hash: $('Crypto').item.json.hash,\n    file_size: $('Prepare File Info').item.json.fileSize,\n    mime_type: $('Prepare File Info').item.json.mimeType\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "supa-insert-hash",
      "name": "Insert New Hash",
      "type": "n8n-nodes-base.supabase",
      "position": [
        2016,
        96
      ],
      "parameters": {
        "tableId": "file_hashes",
        "dataToSend": "autoMapInputData"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "code-handle-db-error",
      "name": "Handle DB Error",
      "type": "n8n-nodes-base.code",
      "position": [
        2240,
        96
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Check if the insert was successful or if a conflict/error occurred\nconst item = $input.item.json;\n\nif (item.error) {\n  return {\n    json: {\n      status: 'duplicate_race_condition',\n      message: 'Concurrent upload detected - hash already exists',\n      error: item.error\n    }\n  };\n}\n\n// If the response doesn't contain an id, it may be a conflict\nif (!item.id && item.id !== 0) {\n  return {\n    json: {\n      status: 'duplicate_race_condition',\n      message: 'Hash already existed (conflict detected)',\n      fileId: $('Prepare File Info').item.json.fileId,\n      fileName: $('Prepare File Info').item.json.fileName\n    }\n  };\n}\n\nreturn {\n  json: {\n    status: 'unique_inserted',\n    insertedId: item.id,\n    fileId: $('Prepare File Info').item.json.fileId,\n    fileName: $('Prepare File Info').item.json.fileName\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "code-prep-unique-log",
      "name": "Prepare Unique Log Data",
      "type": "n8n-nodes-base.code",
      "position": [
        2464,
        96
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Prepare data for Supabase insert into dedup_audit_log\nconst status = $json.status;\nreturn {\n  json: {\n    file_id: $('Prepare File Info').item.json.fileId,\n    file_name: $('Prepare File Info').item.json.fileName,\n    hash: $('Crypto').item.json.hash,\n    status: status,\n    action_taken: status === 'unique_inserted' ? 'stored_hash' : 'race_condition_caught'\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "supa-log-unique",
      "name": "Log Unique Event",
      "type": "n8n-nodes-base.supabase",
      "position": [
        2688,
        96
      ],
      "parameters": {
        "tableId": "dedup_audit_log",
        "dataToSend": "autoMapInputData"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "code-missing-binary",
      "name": "Handle Missing Binary",
      "type": "n8n-nodes-base.code",
      "position": [
        880,
        -16
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const fileInfo = $('Prepare File Info').item.json;\n\nreturn {\n  json: {\n    status: 'error',\n    errorType: 'missing_binary',\n    message: `Binary data missing for file: ${fileInfo.fileName} (${fileInfo.fileId})`,\n    fileId: fileInfo.fileId,\n    fileName: fileInfo.fileName,\n    mimeType: fileInfo.mimeType,\n    timestamp: new Date().toISOString()\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "code-prep-error-log",
      "name": "Prepare Error Log Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1088,
        -16
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Prepare data for Supabase insert into dedup_audit_log\nreturn {\n  json: {\n    file_id: $json.fileId,\n    file_name: $json.fileName,\n    hash: null,\n    status: 'error',\n    action_taken: 'none',\n    error_message: $json.message\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "supa-log-error",
      "name": "Log Error Event",
      "type": "n8n-nodes-base.supabase",
      "position": [
        1312,
        -16
      ],
      "parameters": {
        "tableId": "dedup_audit_log",
        "dataToSend": "autoMapInputData"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "83d46f79-7eb1-4915-b817-695fb32e4e73",
      "name": "No Operation, do nothing",
      "type": "n8n-nodes-base.noOp",
      "position": [
        1568,
        -352
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "9728263e-e319-41bb-b6a7-fef61060399b",
      "name": "Same File Re-triggered?",
      "type": "n8n-nodes-base.if",
      "position": [
        1344,
        -256
      ],
      "parameters": {
        "options": {
          "ignoreCase": false
        },
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "dup-check",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.hash }}",
              "rightValue": 0
            },
            {
              "id": "b2521d34-e9bd-4b07-9333-fac03c8a64b0",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $('Check Hash Exists').item.json.file_id }}",
              "rightValue": "={{ $('Prepare File Info').item.json.fileId }}"
            }
          ]
        }
      },
      "typeVersion": 2.3,
      "alwaysOutputData": false
    },
    {
      "id": "if-duplicate",
      "name": "Hash Matched Different File?",
      "type": "n8n-nodes-base.if",
      "position": [
        1568,
        -144
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "dup-check",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.file_id }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2.3,
      "alwaysOutputData": false
    },
    {
      "id": "5b469c4c-c982-4768-b5ed-25024cba109f",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        -880
      ],
      "parameters": {
        "width": 656,
        "height": 544,
        "content": "This workflow detects and handles duplicate files uploaded to a specific Google Drive folder using content based hashing. Instead of relying on file names, it generates an MD5 hash from the actual file binary and compares it against a Supabase database.\n\nIf the same hash already exists for a different file, the new file is automatically moved to a Duplicates folder and a Slack alert is sent. If the file is unique, its hash is stored in Supabase. All outcomes, including duplicates, successful inserts, race conditions, and binary errors, are logged in an audit table.\n\n## How it works\n\n1. Watches a Google Drive folder for new files.\n2. Downloads the file and generates an MD5 hash.\n3. Checks Supabase for an existing hash match.\n4. Moves duplicate files and sends Slack alerts.\n5. Stores unique hashes and logs all events.\n\n## Setup steps\n\n1. Connect Google Drive and select the folder to monitor.\n2. Connect Supabase and create `file_hashes` and `dedup_audit_log` tables.\n3. Connect Slack and choose a notification channel.\n4. Set your Duplicates folder ID in the Move node."
      },
      "typeVersion": 1
    },
    {
      "id": "b692e01c-32b3-40fe-9087-7a241f052d05",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        -272
      ],
      "parameters": {
        "color": 7,
        "width": 880,
        "height": 352,
        "content": "Watches a specific Google Drive folder for new uploads, extracts metadata, downloads the file, and validates that binary data exists before hashing."
      },
      "typeVersion": 1
    },
    {
      "id": "fd0d975f-a6f4-496d-842b-1f04693c3540",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        848,
        -448
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 352,
        "content": "Generates an MD5 hash from the file binary and checks Supabase to determine whether the hash already exists."
      },
      "typeVersion": 1
    },
    {
      "id": "a8d32a63-100b-4768-8e25-65076003e777",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1280,
        -448
      ],
      "parameters": {
        "color": 7,
        "width": 864,
        "height": 352,
        "content": "If the hash exists for a different file, the workflow moves the file to a Duplicates folder and sends a Slack notification."
      },
      "typeVersion": 1
    },
    {
      "id": "728ec4a7-cac6-4d9b-836b-6f89f3b0bd66",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1760,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 272,
        "content": "If the hash does not exist, the workflow inserts the new hash into Supabase and confirms successful storage, including race condition handling."
      },
      "typeVersion": 1
    },
    {
      "id": "7341d2ae-00fa-4aaa-ade2-2626851c31b4",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2416,
        -432
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 720,
        "content": "Logs duplicates, unique inserts, race conditions, and binary errors into a Supabase audit table for traceability."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Crypto": {
      "main": [
        [
          {
            "node": "Check Hash Exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download File": {
      "main": [
        [
          {
            "node": "Validate Binary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Handle DB Error": {
      "main": [
        [
          {
            "node": "Prepare Unique Log Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert New Hash": {
      "main": [
        [
          {
            "node": "Handle DB Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Binary": {
      "main": [
        [
          {
            "node": "Crypto",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Missing Binary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notify Duplicate": {
      "main": [
        [
          {
            "node": "Prepare Dup Log Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Hash Exists": {
      "main": [
        [
          {
            "node": "Same File Re-triggered?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare File Info": {
      "main": [
        [
          {
            "node": "Download File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Hash Data": {
      "main": [
        [
          {
            "node": "Insert New Hash",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Move to Duplicates": {
      "main": [
        [
          {
            "node": "Notify Duplicate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Drive Trigger": {
      "main": [
        [
          {
            "node": "Prepare File Info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Dup Log Data": {
      "main": [
        [
          {
            "node": "Log Duplicate Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Handle Missing Binary": {
      "main": [
        [
          {
            "node": "Prepare Error Log Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Error Log Data": {
      "main": [
        [
          {
            "node": "Log Error Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Unique Log Data": {
      "main": [
        [
          {
            "node": "Log Unique Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Same File Re-triggered?": {
      "main": [
        [
          {
            "node": "No Operation, do nothing",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Hash Matched Different File?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hash Matched Different File?": {
      "main": [
        [
          {
            "node": "Move to Duplicates",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Hash Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}