Vai al contenuto
HexaTransfer
Torna al blog
Approfondimenti tecnici

Crea il tuo servizio di trasferimento file: tutorial per sviluppatori

Costruisci un servizio di trasferimento file da zero. Design dell'API backend, integrazione dello storage e sviluppo dell'interfaccia utente.

Il GDPR impone la minimizzazione dei dati — e un servizio di trasferimento file senza account, senza log degli IP e con cancellazione automatica alla scadenza è strutturalmente conforme fin dalla progettazione. Per costruire il proprio servizio servono cinque componenti: un frontend che suddivide e facoltativamente cifra i file nel browser, una REST API che crea sessioni di upload e presegna gli URL di storage, object storage compatibile S3 (Cloudflare R2, Backblaze B2 o MinIO), un piccolo database di metadati (SQLite o Postgres), e un reverse proxy TLS 1.3. Con Node 22, Fastify e Vite si può realizzare un servizio in stile WeTransfer in circa 600 righe di codice e ospitarlo per 5-15 € al mese a traffico moderato.

Scegliere prima lo strato di storage

L'economia dello storage determina il successo o il fallimento di un servizio di trasferimento file. AWS S3 addebita 0,09 $ per GB di egress, il che significa che un download da 2 GB condiviso 1.000 volte costa 180 $ solo di banda. Cloudflare R2 non addebita nulla per l'egress e costa 0,015 $ per GB archiviato. Backblaze B2 addebita 0,01 $ per GB di egress e 0,006 $ per GB archiviato. Per un servizio di trasferimento gratuito, R2 è la scelta ovvia. MinIO consente di ospitare la semantica S3 sul proprio hardware, il che funziona se si dispone già di banda e dischi inutilizzati.

Usa gli URL presegnati affinché i client carichino e scarichino direttamente dallo storage, mai attraverso il tuo server. Questo mantiene la tua API vincolata alla CPU invece che alla banda.

Definire la superficie dell'API

Mantieni l'API piccola. Cinque endpoint coprono il 95% di ciò che serve:

  • POST /sessions — crea una sessione di upload, restituisce ID e URL multipart presegnati
  • POST /sessions/:id/complete — finalizza un upload multipart, restituisce l'URL di condivisione
  • GET /shares/:id — restituisce i metadati del file (dimensione, scadenza, download rimanenti)
  • GET /shares/:id/download — restituisce un URL di download presegnato
  • DELETE /shares/:id — consente al proprietario di revocare anticipatamente (richiede un token di revoca)

Limita aggressivamente le richieste a POST /sessions — 20 per IP per ora è sufficiente. Tutti gli endpoint restituiscono JSON, usano Cache-Control: no-store e impostano Content-Security-Policy sulle pagine HTML.

Collegare il backend con Fastify

Fastify offre validazione dello schema JSON, logging sensato e 30k richieste/s per core senza sforzo. Un POST /sessions minimale:

fastify.post('/sessions', {
  schema: {
    body: {
      type: 'object', required: ['size'],
      properties: {
        size: { type: 'integer', minimum: 1, maximum: 10737418240 },
        contentType: { type: 'string', maxLength: 120 }
      }
    }
  }
}, async (req) => {
  const id = nanoid(16);
  const key = `blobs/${id}`;
  const upload = await s3.createMultipartUpload({
    Bucket: 'transfers', Key: key, ContentType: req.body.contentType
  });
  const partCount = Math.ceil(req.body.size / (8 * 1024 * 1024));
  const urls = await Promise.all(
    Array.from({ length: partCount }, (_, i) =>
      getSignedUrl(s3, new UploadPartCommand({
        Bucket: 'transfers', Key: key, UploadId: upload.UploadId, PartNumber: i + 1
      }), { expiresIn: 3600 }))
  );
  db.prepare('INSERT INTO sessions VALUES (?, ?, ?, ?)')
    .run(id, upload.UploadId, key, Date.now());
  return { id, uploadId: upload.UploadId, partUrls: urls };
});

Limite di 10 GB, parti da 8 MB, validità URL di 1 ora. Adatta in base al tuo mix di traffico.

Costruire l'interfaccia di upload

Vite più React più zero librerie UI porta a un drag-and-drop funzionante in circa 150 righe. Il componente critico è il worker di upload: avvia un Web Worker dedicato, passagli il File handle e gli URL delle parti, e fallo eseguire PUT di ogni blocco con concorrenza a 3. Il thread principale rimane reattivo, gli aggiornamenti sul progresso arrivano tramite postMessage.

// worker.js
self.onmessage = async ({ data: { file, partUrls } }) => {
  const partSize = 8 * 1024 * 1024;
  const etags = [];
  for (let i = 0; i < partUrls.length; i++) {
    const blob = file.slice(i * partSize, (i + 1) * partSize);
    const res = await fetch(partUrls[i], { method: 'PUT', body: blob });
    etags[i] = res.headers.get('etag');
    self.postMessage({ type: 'progress', done: i + 1, total: partUrls.length });
  }
  self.postMessage({ type: 'done', etags });
};

Aggiungi un campo password che deriva una chiave tramite PBKDF2 (600.000 iterazioni, SHA-256) e passa ogni blocco attraverso AES-256-GCM prima del PUT. Questo trasforma il servizio da un servizio cifrato lato server a un servizio con crittografia end-to-end.

Progettare lo schema del database

SQLite tramite better-sqlite3 gestisce decine di milioni di righe senza problemi. Mantieni lo schema snello:

CREATE TABLE sessions (
  id TEXT PRIMARY KEY, upload_id TEXT, object_key TEXT, created_at INTEGER
);
CREATE TABLE shares (
  id TEXT PRIMARY KEY, object_key TEXT, size_bytes INTEGER,
  content_type TEXT, expires_at INTEGER, max_downloads INTEGER,
  download_count INTEGER DEFAULT 0, revocation_token TEXT
);
CREATE INDEX idx_shares_expires ON shares(expires_at);

Nessuna tabella utente, nessuna email, nessun log degli IP. Questa è una scelta deliberata — rende la conformità al GDPR quasi banale. Un cron notturno elimina le righe scadute e i loro oggetti S3:

const expired = db.prepare('SELECT object_key FROM shares WHERE expires_at < ?').all(Date.now());
for (const { object_key } of expired) {
  await s3.deleteObject({ Bucket: 'transfers', Key: object_key });
}
db.prepare('DELETE FROM shares WHERE expires_at < ?').run(Date.now());

Implementare i link di condivisione e i download

Gli URL di condivisione hanno l'aspetto https://tuaapp.com/f/abc123#k=<chiave-base64> quando si aggiunge la crittografia lato client. Il frammento mantiene la chiave fuori dai log del server. La pagina di download recupera /shares/abc123, ottiene un URL S3 presegnato, trasmette i byte direttamente da R2, decifra nel browser e consegna il risultato a StreamSaver.js o all'API File System Access.

Imposta Content-Disposition: attachment; filename*=UTF-8''nome-codificato affinché il browser avvii una finestra di salvataggio invece di renderizzare il file inline — importante per PDF, HTML e SVG, che possono eseguire script nel contesto del browser.

Indurire il servizio contro gli abusi

I servizi di trasferimento file attraggono abusatori. Aspettati tre categorie: distribuzione di malware, hosting di payload di phishing e upload che violano il diritto d'autore. Contromisure in ordine di impatto: limita gli upload per IP, richiedi una dimensione minima (sotto 10 KB è quasi sempre spazzatura), calcola uno SHA-256 del testo cifrato e confrontalo con una lista nera di hash noti e problematici, e fornisci un modulo di segnalazione abusi senza attriti. ClamAV sul contenuto decifrato funziona solo quando si ha la chiave, il che vanifica la crittografia end-to-end — la risposta corretta è il rate limiting agnostico del contenuto più takedown reattivi.

Distribuire ed eseguire a basso costo

Un singolo Hetzner CX22 da 10 € (2 vCPU, 4 GB RAM) esegue Fastify + SQLite + un reverse proxy Caddy e gestisce migliaia di utenti attivi. Caddy fornisce TLS 1.3 automaticamente tramite Let's Encrypt. Metti Cloudflare davanti per la protezione DDoS e il WAF gratuito. Monitora con Uptime Kuma (gratuito, self-hosted) e logga su stdout catturato da journald.

Per la ridondanza geografica, replica SQLite con Litestream su R2, e punta una seconda regione sullo stesso object store. Il failover è a livello DNS.

L'intero approccio sopra rispecchia come è costruito HexaTransfer — API Fastify, storage R2, Web Crypto nel browser, metadati SQLite, nessun account. Prova su https://hexatransfer.com — gratuito, senza account, massimo 10 GB.

Cosa rimandare a quando serve davvero

Non costruire account, team o cartelle nella v1. Non aggiungere l'invio di email. Non implementare anteprime video o miniature di immagini (entrambi costringono il server a vedere il testo in chiaro). Non rilasciare un'app mobile prima che la versione web sia solida su Safari mobile. Il feature creep che ha ucciso i primi cloni di servizi di trasferimento file ha sempre seguito lo stesso schema: account, poi quote di storage, poi pagamento, poi piani team, e improvvisamente ci si trova in competizione con Dropbox invece di consegnare uno strumento di trasferimento rapido e privato.

Invia file di grandi dimensioni in modo sicuro con crittografia end-to-end

Trasferisci file fino a 10 GB gratuitamente con crittografia end-to-end. Nessun account necessario. I tuoi file vengono crittografati nel browser prima del caricamento — nessun altro può leggerli.

Invia un file