AutomationFlowsGeneral › NixFrame Upload Handler Webhook

NixFrame Upload Handler Webhook

Original n8n title: Nixframe Upload Handler

NixFrame Upload Handler. Webhook trigger; 5 nodes.

Webhook trigger★★★★☆ complexity5 nodes
General Trigger: Webhook Nodes: 5 Complexity: ★★★★☆ Added:

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "nixframe-upload",
  "name": "NixFrame Upload Handler",
  "nodes": [
    {
      "id": "upload-webhook",
      "name": "Upload Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        100,
        300
      ],
      "parameters": {
        "path": "nixframe-upload",
        "httpMethod": "POST",
        "responseMode": "responseNode",
        "options": {}
      }
    },
    {
      "id": "validate-and-save",
      "name": "Validate and Save",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        300,
        300
      ],
      "parameters": {
        "jsCode": "const fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst { execFileSync } = require('child_process');\n\nconst PHOTO_DIR = '/var/lib/nixframe/photos';\nconst MAX_SIZE = 20 * 1024 * 1024; // 20MB\nconst ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif'];\n\ntry {\n  const body = $input.first().json.body || {};\n  const imageData = body.imageData;\n\n  if (!imageData || typeof imageData !== 'string') {\n    return [{ json: { success: false, error: 'Missing imageData field' } }];\n  }\n\n  // Extract base64 data and MIME type\n  let mimeType = 'image/jpeg';\n  let base64Data = imageData;\n\n  if (imageData.startsWith('data:')) {\n    const match = imageData.match(/^data:(image\\/[^;]+);base64,(.+)$/);\n    if (!match) {\n      return [{ json: { success: false, error: 'Invalid data URL format' } }];\n    }\n    mimeType = match[1];\n    base64Data = match[2];\n  }\n\n  // Validate MIME type\n  if (!ALLOWED_MIME.includes(mimeType)) {\n    return [{ json: { success: false, error: 'Unsupported format: ' + mimeType + '. Use JPG, PNG, WebP, or HEIC.' } }];\n  }\n\n  // Decode base64\n  const buffer = Buffer.from(base64Data, 'base64');\n\n  // Validate size\n  if (buffer.length > MAX_SIZE) {\n    return [{ json: { success: false, error: 'File too large (' + Math.round(buffer.length / 1024 / 1024) + 'MB). Max 20MB.' } }];\n  }\n\n  // Generate unique filename with timestamp\n  const hash = crypto.createHash('sha256').update(buffer).digest('hex').substring(0, 8);\n  const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);\n  const tmpId = crypto.randomUUID();\n\n  // Determine input extension for ImageMagick\n  const isHeic = mimeType === 'image/heic' || mimeType === 'image/heif';\n  const inputExt = isHeic ? '.heic' : (mimeType === 'image/png' ? '.png' : (mimeType === 'image/webp' ? '.webp' : '.jpg'));\n  const outputExt = '.jpg'; // Always output JPEG for consistency\n\n  // Write raw input to temp file\n  const tmpInput = path.join(PHOTO_DIR, '.tmp-input-' + tmpId + inputExt);\n  const tmpOutput = path.join(PHOTO_DIR, '.tmp-output-' + tmpId + outputExt);\n  const finalName = timestamp + '_' + hash + outputExt;\n  const finalPath = path.join(PHOTO_DIR, finalName);\n\n  fs.writeFileSync(tmpInput, buffer);\n\n  try {\n    // Convert: auto-orient EXIF, strip metadata, convert HEIC\u2192JPEG\n    // Uses execFileSync (no shell) to avoid command injection risks\n    try {\n      execFileSync('convert', [tmpInput, '-auto-orient', '-strip', tmpOutput], { timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] });\n    } catch (convertErr) {\n      const stderr = convertErr.stderr ? convertErr.stderr.toString() : '';\n      if (convertErr.killed || convertErr.signal === 'SIGTERM') {\n        return [{ json: { success: false, error: 'Image conversion timed out. Try uploading a smaller image or convert HEIC to JPEG first.' } }];\n      }\n      if (stderr.includes('no decode delegate') || stderr.includes('unable to open')) {\n        return [{ json: { success: false, error: 'Could not process this image format. Try converting to JPEG before uploading.' } }];\n      }\n      return [{ json: { success: false, error: 'Image conversion failed: ' + (stderr.split('\\n')[0] || convertErr.message) } }];\n    }\n\n    // Atomic rename to final location (prevents watcher from seeing partial file)\n    fs.renameSync(tmpOutput, finalPath);\n    fs.chmodSync(finalPath, 0o664);\n\n    // Signal systemd.paths watcher \u2014 atomic rename() causes systemd to lose\n    // the inotify watch on the new file (systemd bug #20934), so we use a trigger file\n    fs.writeFileSync(path.join(PHOTO_DIR, '.trigger'), Date.now().toString());\n  } finally {\n    // Clean up temp files (ENOENT is expected if conversion succeeded)\n    try { fs.unlinkSync(tmpInput); } catch (e) { if (e.code !== 'ENOENT') console.error('Failed to clean temp input:', e.message); }\n    try { fs.unlinkSync(tmpOutput); } catch (e) { if (e.code !== 'ENOENT') console.error('Failed to clean temp output:', e.message); }\n  }\n\n  // Remove placeholder if real photos now exist\n  const placeholderPath = path.join(PHOTO_DIR, '000-placeholder.png');\n  try {\n    const files = fs.readdirSync(PHOTO_DIR).filter(f => /\\.(jpg|jpeg|png|webp)$/i.test(f) && !f.startsWith('.') && f !== '000-placeholder.png');\n    if (files.length > 0 && fs.existsSync(placeholderPath)) {\n      try { fs.unlinkSync(placeholderPath); } catch (unlinkErr) { console.error('Failed to remove placeholder:', unlinkErr.message); }\n    }\n  } catch (dirErr) {\n    if (dirErr.code !== 'ENOENT') console.error('Failed to list photo directory for placeholder cleanup:', dirErr.message);\n  }\n\n  // Count photos for response\n  const photoCount = fs.readdirSync(PHOTO_DIR).filter(f => /\\.(jpg|jpeg|png|webp)$/i.test(f) && !f.startsWith('.')).length;\n\n  return [{ json: { success: true, filename: finalName, photoCount } }];\n\n} catch (err) {\n  return [{ json: { success: false, error: 'Upload failed: ' + (err.message || 'Unknown error') } }];\n}\n"
      }
    },
    {
      "id": "check-success",
      "name": "Check Success",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        500,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "is-success",
              "leftValue": "={{ $json.success }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "success-response",
      "name": "Success Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        700,
        200
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: true, filename: $json.filename, photoCount: $json.photoCount }) }}",
        "options": {
          "responseCode": 200
        }
      }
    },
    {
      "id": "error-response",
      "name": "Error Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        700,
        400
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, error: $json.error }) }}",
        "options": {
          "responseCode": 400
        }
      }
    }
  ],
  "connections": {
    "Upload Webhook": {
      "main": [
        [
          {
            "node": "Validate and Save",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate and Save": {
      "main": [
        [
          {
            "node": "Check Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Success": {
      "main": [
        [
          {
            "node": "Success Response",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": true,
  "versionId": "a7a03d95-685d-4358-bdd7-1173182103b7"
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

NixFrame Upload Handler. Webhook trigger; 5 nodes.

Source: https://github.com/alexandru-savinov/nixos-config/blob/df04cb96298f2fc48c7442f31044aae1e7fae571/n8n-workflows/nixframe-upload.json — original creator credit. Request a take-down →

More General workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

General

A production-ready authentication workflow implementing secure user registration, login, token verification, and refresh token mechanisms. Perfect for adding authentication to any application without

Crypto, Data Table, Execute Workflow Trigger
General

Portfolio Orchestrator. Uses httpRequest. Webhook trigger; 59 nodes.

HTTP Request
General

This n8n template demonstrates how a simple Multi-Layer Perceptron (MLP) neural network can predict housing prices. The prediction is based on four key features, processed through a three-layer model.

General

github code Try yourself

Google Calendar
General

This workflow contains community nodes that are only compatible with the self-hosted version of n8n.

N8N Nodes 1Shot