8000 GitHub - tedli/filedrop · GitHub
[go: up one dir, main page]

Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

filedrop — rsync-over-HTTP

English | 简体中文

A pure-browser, HTTP-based large-file transfer server. No client tools needed — just open a browser and start uploading.

tl;dr: Browser-based rsync for transferring multi-GB files, folders, and resuming after interruption. Zero dependencies on the client side.

Why filedrop

A browser-side rsync purpose-built for Kubernetes + shared storage — no client install, no central session store, no plugin runtime; authentication is injected at compile time through a Rust trait.

  • mtime-based session lease — upload sessions live in the .part file's mtime on the shared storage itself: zero coordination between replicas, no session store, no external lock service.
  • Zero-build embedded debug UI — the complete single-page frontend is baked into the binary via rust-embed; no Node/npm toolchain anywhere.
  • Rust-trait extensibility — auth/path mapping (RequestHook) and response headers (ResponseHook) compile into your own binary: no FFI, no GC runtime, no hot-reload plugin machinery.
  • Small and safe — zero unsafe in the codebase, distroless container image, ~3 MB stripped release binary.

filedrop solves transport only. Product business logic — authentication, tenancy, quota, rate limiting — is implemented by your deployment through the hook traits, never inside filedrop.

Features

  • GB-scale uploads — chunked at 2 MiB, serial chunks per file (the built-in UI adds 3-file client-side concurrency)
  • Resume after interruption — .part temp files + chunk-aligned offsets; next scan picks up where it left off
  • Incremental skip — SHA-256 hash match → skip unchanged files entirely
  • Folder transfer — drag a folder, directory structure is preserved
  • Directory listing — GET a directory to list its first level as JSON
  • Real-time speed — per-file live speed plus total up/down rate (U 1.2 MB/s · D 3.4 MB/s), in the toolbar and the floating widget
  • Background transfer — minimize to a draggable floating widget; keep browsing
  • Bulk cancel + resume — cancel all, resume later; cancel one file, others continue
  • Range download — RFC 7233 single-range support; compatible with curl -C -, wget -c, aria2c -c
  • Delete — DELETE a file, or recursively delete a directory; the dev UI adds checkboxes + a delete-selected action
  • Partial-upload cleanup — interrupted uploads show up in the listing (partial:true, with progress); deletable unless an active session is still feeding them (409, replica-safe via shared-storage mtime); deliberate cancels release the session immediately (PATCH)
  • RESTful API — resource path in the URL, operation selected by HTTP method
  • Compile-time pluggable auth + path mapping — implement one RequestHook trait (JWT, SSO, tenant path prefixes, …) in your own crate and build your own binary against the filedrop library
  • Graceful shutdown — waits for in-flight requests, 10-second fallback
  • Zero client install — single HTML file embedded in the binary

Who is this for

Built for AI/ML platforms where algorithm engineers move multi-GB datasets and model weights onto cluster storage:

  • They are not systems operators — mastering rsync/sftp (keys, flags, retries) is not their job.
  • Dev machines are personal laptops running different OSes; the browser is the one client that is always there — zero extra tooling.
  • Storage is a shared filesystem (CephFS, NFS, GlusterFS, … behind Kubernetes PVCs) mounted by training/inference pods. A RequestHook resolves the target PVC per request — e.g. a query parameter names the PVC, a JWT yields the user/tenant, and the hook maps namespace → PVC → PV → a path relative to the filedrop root — so browser uploads (the dominant direction) land directly inside the PVC the pods mount.

Quick Start

# Clone and build (requires Rust 1.87+ — `rust-version` in Cargo.toml)
git clone https://github.com/infrawaves-venus/filedrop.git
cd filedrop
cargo build --release

# Run (development mode — built-in allow-all auth, URL-path passthrough)
FILEDROP_ROOT=./data ./target/release/filedrop

# Open browser: http://localhost:8080

Drag files or folders into the browser, click Start Transfer, and you're done.

Configuration

All configuration is via environment variables:

Variable Default Description
FILEDROP_ADDR 127.0.0.1:8080 Listen address. Loopback-only by default — set 0.0.0.0:8080 explicitly to expose the server beyond the local machine (the Dockerfile does this; the default PassthroughHook logs a warning on non-loopback binds)
FILEDROP_ROOT ./data Storage root directory (production: mount the shared filesystem here). The server refuses to start if it resolves to a filesystem root (/, C:\, …) or the home directory — filedrop serves and deletes everything under the root
FILEDROP_CHUNK_SIZE 2097152 Upload chunk size in bytes (must match frontend; frontend reads it from GET /config)
FILEDROP_UPLOAD_ACTIVE_TTL_SECS 60 mtime freshness window that classifies a partial upload as active (listed as active:true, protected from DELETE with 409)
FILEDROP_CORS_ALLOW_ORIGINS (unset) Comma-separated allowed origins, or * for any. Unset/empty leaves CORS off — no CORS layer is mounted and every other FILEDROP_CORS_* variable is ignored. When set, filedrop handles CORS itself (preflight included), so you don't have to terminate it at an ingress/gateway
FILEDROP_CORS_ALLOW_METHODS (filedrop's methods) Comma-separated allowed methods, or *. Empty falls back to GET, HEAD, PUT, POST, PATCH, DELETE
FILEDROP_CORS_ALLOW_HEADERS (mirror request) Comma-separated allowed request headers, or *. Empty mirrors the preflight's Access-Control-Request-Headers
FILEDROP_CORS_EXPOSE_HEADERS (none) Comma-separated response headers exposed to browser JS, or *. Set this (e.g. upload-offset) if a browser client must read filedrop's non-safelisted response headers
FILEDROP_CORS_ALLOW_CREDENTIALS false true/1/yes/on to send Access-Control-Allow-Credentials: true. Requires explicit (non-*) origins/methods/headers — combining it with * is refused at startup (Fetch standard)
FILEDROP_CORS_MAX_AGE_SECS (unset) Access-Control-Max-Age in seconds (preflight cache lifetime); omitted when unset

FILEDROP_CHUNK_SIZE and FILEDROP_UPLOAD_ACTIVE_TTL_SECS fall back to their defaults (with an INFO log) when set to an unparseable value or 0 — a zero chunk size or TTL would break chunk alignment / the lease check.

CORS is applied as the outermost layer, ahead of the /files request hook, so a browser preflight (which carries no credentials) is answered before auth can reject it. Leave FILEDROP_CORS_ALLOW_ORIGINS unset if you terminate CORS at an ingress or gateway.

Architecture

Request → request_hook → validate_path → track_requests → handler → response_hook
  1. Request Hook (RequestHook): a single combined authn/authz + path-mapping stage. It verifies the request (token, claims, …) and maps the URL path (the part after /files/, percent-decoded) to a storage-relative path, which is stored on the request context (RawPath) for the stages below. A rejection terminates the stack with the hook-supplied HTTP status code (401, 403, …) and error message. Default: PassthroughHook allows everything and uses the URL path verbatim.
  2. Validate Path: lexically cleans the path, rejects .. that escapes the storage root, and additionally walks the existing components of the cleaned path to reject any symlink (on Windows: any reparse point, junctions included) — a link planted inside the root pointing outside it is invisible to lexical cleaning alone. The result is stored as ValidatedPath for handlers. This is not pluggable — it's a hard requirement, never left to hook logic.
  3. Track Requests: counts active requests for graceful shutdown.
  4. Response Hook (ResponseHook): the outermost stage, invoked once per response (UI, assets, config, probes and /files alike) with the request path, method and final status. It may add/replace/remove response headers only — the streaming /files body is never touched. Default: DefaultSecurityHook sends X-Content-Type-Options: nosniff and X-Frame-Options: DENY on every response, plus Content-Security-Policy: default-src 'none'; sandbox on GET /files/* (so downloaded content can never execute), while leaving the debug UI's inline handlers working.

Auth and path mapping share one hook deliberately: the mapping usually depends on the identity extracted during auth (user id, tenant claim), so any intermediate context stays inside the hook implementation — no cross-plugin context passing.

API

The resource (a file or directory) is identified by its path in the URL under /files/. The HTTP method selects the operation.

Method Path Operation
GET /files/{path} Download a file (Range), or list a directory
HEAD /files/{path} Metadata / resumable-upload progress
PUT /files/{path} Upload one chunk
PATCH /files/{path} Release the upload session on a partial (mark idle)
DELETE /files/{path} Delete a file, or recursively delete a directory
POST /files/{base} Reconcile an upload manifest under {base} (scan)

Auxiliary endpoints: GET /config (frontend config + version info), GET /healthz, GET /readyz.

GET /files/{path} — download

# Full file
curl -O -J http://localhost:8080/files/models/llama3.bin

# Resume from byte 1048576
curl -C - -O -J http://localhost:8080/files/models/llama3.bin \
  -H "Range: bytes=1048576-"

Every file response carries Content-Disposition: attachment; filename="..." plus the RFC 5987 filename*=UTF-8''... form (percent-encoded UTF-8, the same convention as nginx and S3), so non-ASCII filenames survive the round-trip in every browser. A Range that cannot be satisfied (malformed, or start beyond EOF) returns 416 Range Not Satisfiable with Content-Range: bytes */<size>. A zero-byte file downloads as a plain 200 with Content-Length: 0; any Range request against it returns 416 with Content-Range: bytes */0 — an empty file has no satisfiable byte.

GET /files/{dir} — list a directory

When {path} is a directory the response is a JSON listing of its first level (non-recursive):

curl http://localhost:8080/files/models
# → [{"name":"sub","dir":true,"size":0,"mtime":1700000000},{"name":"llama3.bin","dir":false,"size":1073741824,"mtime":1700000000}]

GET /files lists the storage root.

Interrupted uploads staged under .filedrop_tmp/ appear as extra entries with "partial":true, listed under their original file name so the space they hold is visible and reclaimable:

{"name":"big.bin","dir":false,"size":314572800,"mtime":1700000000,
 "partial":true,"active":false,"total":2147483648}
  • size — bytes received so far; total — expected final size (from the .meta sidecar), so a client can render a percentage.
  • active — the .part mtime is fresher than FILEDROP_UPLOAD_ACTIVE_TTL_SECS, meaning some replica is still receiving chunks for it. mtime comes from the shared storage, so the judgement is consistent across all replicas.

HEAD /files/{path} — status

curl -I http://localhost:8080/files/models/llama3.bin

Response headers:

  • Upload-Offset — chunk-aligned bytes already received (resume point)
  • X-Upload-Complete — true if the final file exists, false for a .part
  • complete files also return Content-Length and Accept-Ranges: bytes
  • 404 if neither the final file nor a .part exists

PUT /files/{path} — upload one chunk

curl -X PUT http://localhost:8080/files/models/llama3.bin \
  -H "Content-Range: bytes 0-2097151/1073741824" \
  -H "X-SHA256: a1b2c3..." \
  --data-binary @chunk.bin

Content-Range: bytes {start}-{end}/{total} gives the offset and total file size; X-SHA256 is the expected digest of the whole file (verified when the last chunk lands, before the .part is renamed into place). The server maintains an incremental hash midstate in the .meta sidecar as chunks arrive — shared-storage-resident, so it works across replicas — and an uninterrupted upload finalises without re-reading the staged file. A restart from byte 0 rebuilds the midstate as the bytes arrive again; only a mid-stream rewrite of already-hashed bytes degrades finalise to the full re-read fallback.

Response: {"ok":true,"offset":0,"complete":false,"next_offset":2097152}, plus an Upload-Offset header carrying the same value as next_offset — the chunk-aligned resume point after this write (identical to what HEAD would report), so a client that lost track of its position can self-correct mid-stream. On the final chunk complete is true and next_offset equals the total size.

Don't guess the starting offset. Before the first PUT — and after any interruption — ask the server where to start: HEAD returns Upload-Offset and a scan returns offset; continue from there, and follow the Upload-Offset echoed by each PUT response.

A chunk body larger than the configured FILEDROP_CHUNK_SIZE is rejected with 413 Payload Too Large; the error message names the configured limit. The Content-Range header must satisfy start <= end < total and the body must carry exactly the end - start + 1 bytes it announces — anything else is 400 Bad Request, checked before a single byte is staged, so a truncated chunk can't leave a hole that surfaces as a hash mismatch much later. The Content-Range total must stay constant across all chunks of one file: a chunk announcing the same X-SHA256 but a different total is rejected with 400 Bad Request. (A chunk announcing a different hash is a competing upload instead: 409 Conflict while the existing staging session is active.) If the underlying filesystem write fails because storage is exhausted — EDQUOT (disk quota exceeded) or ENOSPC (no space left on device) — the server responds 507 Insufficient Storage. Other handler errors return 500 with {"error": "<message>"}.

POST /files/{base} — scan

Check which files need uploading, skipping, or resuming, relative to {base} (omit it to scan from the root):

curl -X POST http://localhost:8080/files \
  -H "Content-Type: application/json" \
  -d '[{"rel_path":"models/llama3.bin","size":1073741824,"sha256":"a1b2c3..."}]'

Response: [{"rel_path":"models/llama3.bin","action":"skip|full|partial","offset":0}]

The manifest body is capped at 8 MiB (SCAN_BODY_LIMIT); a larger manifest is rejected with 413 and a message naming the limit and suggesting smaller batches. A row costs ~200 bytes, so one scan admits tens of thousands of files. The built-in UI stays under the cap by posting the manifest in batches of 5,000 entries and merging the per-batch results.

DELETE /files/{path} — delete

Delete a file, or recursively delete a directory and everything under it:

curl -X DELETE http://localhost:8080/files/models/llama3.bin
curl -X DELETE http://localhost:8080/files/models        # recursive

Returns 204 No Content. Apart from the active-upload guard described below, the server performs no confirmation — the caller is trusted to have confirmed intent (the web UI prompts first). Deleting a missing path still returns 204 (idempotent). Only /files/{path} accepts DELETE; the storage root cannot be deleted.

There is deliberately no batch-delete endpoint: it would break the RESTful mapping (the URL names one resource, the HTTP method is the operation), and the expected flow is a paginated product UI where a human ticks a handful of checkboxes — at that scale a client-side loop issuing one DELETE per path is entirely adequate (the dev UI does exactly that).

Deleting a path also removes its staged partial upload (.part + .meta), so DELETE is how the space held by an interrupted upload is reclaimed. If the partial is still being fed by an active upload session (fresh mtime within FILEDROP_UPLOAD_ACTIVE_TTL_SECS — valid across replicas, since all replicas see the same shared-storage mtime), the server refuses with 409 Conflict. A client that has just cancelled its own upload can bypass the freshness guard with the X-Force-Delete: 1 header (the web UI does this when you cancel an upload and confirm deleting the partial data), or first PATCH the path to release the session and then delete normally.

X-Force-Delete is honoured on the client's word — the server cannot verify that the requester really owns the active session. If that trust is too broad for your deployment, gate it in your RequestHook: the header is visible in the hook's headers argument, so the hook can reject force-deletes (e.g. 403) unless the caller is an admin or holds a specific permission.

PATCH /files/{path} — release a partial upload session

Announce that the upload session feeding {path}'s partial is over (the user cancelled deliberately but keeps the data for a later resume):

curl -X PATCH http://localhost:8080/files/models/llama3.bin

Returns 204 (released) or 404 (no partial exists). The server backdates the .part mtime past the activity window — the mtime is the session lease, chunk writes renew it, PATCH expires it — so every replica immediately lists the partial as active:false and allows a plain DELETE, instead of showing it as an active session for up to another TTL window. Unannounced interruptions (closed tab, crash, network loss) still age out via the TTL. Resuming later is unaffected (the next chunk write renews the lease). Note the listed mtime of a released partial shifts back by the TTL accordingly.

GET /healthz & GET /readyz

Kubernetes probes. readyz returns 503 during graceful shutdown.

GET /config

Server configuration for the built-in frontend:

curl http://localhost:8080/config
# → {"chunk_size":2097152,"upload_active_ttl_secs":60,"git_commit":"bb5ed26...","build_time":"2026-07-09T05:34:27Z"}

The web UI reads chunk_size (so its chunk slicing matches the server) and upload_active_ttl_secs (so its activity wording matches the server's TTL) before uploading. git_commit / build_time are stamped at build time (Makefile/Dockerfile); a plain cargo build reports unknown. The web UI shows the short commit next to the logo, with the full stamp in a tooltip.

Custom Deployments (RequestHook)

filedrop is a library crate with a default binary. The default binary runs PassthroughHook (no auth, URL path used verbatim) and is suitable for development and trusted single-tenant use; it logs a WARN at startup when the server listens on a non-loopback address. A production deployment keeps its business logic (JWT secrets handling, tenant path layout, …) in its own private crate, depends on filedrop, implements RequestHook, and builds its own binary:

use axum::http::{HeaderMap, StatusCode};
use filedrop::{Config, DefaultSecurityHook, Rejection, RequestHook};
use std::sync::Arc;

struct MyHook;

impl RequestHook for MyHook {
    fn handle(
        &self,
        url_path: &str,   // percent-decoded path after `/files/`
        headers: &HeaderMap,
        query: &str,      // URL query string, without leading `?`
    ) -> Result<String, Rejection> {
        // 1. Auth: verify the token; reject with any status + message.
        let user = verify_token(headers)
            .map_err(|_| Rejection::new(StatusCode::UNAUTHORIZED, "invalid token"))?;
        // 2. Path mapping: identity feeds directly into the mapping.
        //    Return the raw storage-relative path; the host validates it.
        Ok(format!("{}/{}", user.tenant, url_path))
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    filedrop::serve(
        Config::from_env()?,
        Arc::new(MyHook),
        Arc::new(DefaultSecurityHook),
    )
    .await
}

A runnable version of this pattern lives in examples/custom_hook.rs (cargo run --example custom_hook).

The hook's returned path is a pure mapping result: the host lexically cleans it and rejects root escapes in the non-pluggable validation middleware, so path safety never depends on hook code. Everything else (Config fields, chunk size, graceful shutdown) works the same as the default binary.

Response Headers (ResponseHook)

serve() also takes a ResponseHook, the outermost stage run once per response across every route. It receives a ResponseCtx (request path, method, and the final status) and a &mut HeaderMap, and may add, replace or remove response headers only — the streaming /files body is off limits by contract, so downloads keep their range/resume semantics.

The shipped DefaultSecurityHook is a sane baseline:

  • X-Content-Type-Options: nosniff and X-Frame-Options: DENY on every response (MIME-sniffing and clickjacking guards).
  • Content-Security-Policy: default-src 'none'; sandbox on GET /files/* only — neutralizes any script/embed inside downloaded user content. It is intentionally omitted from the debug UI, whose inline onclick handlers need a laxer policy.

A deployment classifies routes by inspecting ctx.path (starts_with "/files", "/assets", == "/", …) and can swap in its own policy (custom CSP, HSTS, cache headers, …):

use axum::http::{HeaderMap, HeaderValue, Method, StatusCode};
use filedrop::{ResponseCtx, ResponseHook};

struct MySecurityHook;

impl ResponseHook for MySecurityHook {
    fn handle(&self, ctx: &ResponseCtx, headers: &mut HeaderMap) {
        headers.insert("x-content-type-options", HeaderValue::from_static("nosniff"));
        if ctx.path.starts_with("/files") {
            headers.insert(
                "strict-transport-security",
                HeaderValue::from_static("max-age=63072000"),
            );
        }
    }
}

Pass it as the third argument: filedrop::serve(config, request_hook, Arc::new(MySecurityHook)).

Build & Run

# Toolchain: Rust 1.87+ (rust-version in Cargo.toml — older toolchains refuse to build)
# Development
cargo build
FILEDROP_ROOT=./data cargo run

# Example deployment binary with a custom RequestHook
cargo run --example custom_hook

# Production (smallest binary, LTO)
cargo build --release

# Production run (shared filesystem mounted at the storage root)
FILEDROP_ADDR=0.0.0.0:8080 \
FILEDROP_ROOT=/mnt/shared-fs \
./target/release/filedrop   # or your own binary built against the filedrop crate

# Container image (COPY-based build → distroless cc image, version-stamped)
make            # or: docker build .

Frontend

The frontend lives in ui/ — a thin HTML shell (index.html) plus split assets (assets/styles.css, assets/i18n.js, assets/app.js, the vendored assets/hash-wasm.js, and assets/favicon.svg). The whole directory is baked into the binary at compile time via rust-embed and served at / and /assets/*. No external static file server is needed.

The top bar has a + button to add custom request headers: each click adds a key/value input pair with a − button to remove it. One row is pre-created with the key Authorization (fill in your token, e.g., Bearer eyJ...). Every pair with a non-empty key and value is sent as-is on every /files/* request; rows with an empty key or value send nothing, and duplicate names overwrite earlier ones.

Next to the header inputs, a + button adds custom query params: each click adds a key/value input pair with a − button to remove it. All pairs with a non-empty key are URL-encoded and appended to every /files/* request (and to the generated curl/wget/aria2c commands). Duplicate keys are allowed (names=a&names=b). By default no params are added. Server-side, the query string reaches the RequestHook via its query argument.

Upload pipeline details: hashing streams each file and polls the cancel flags between chunks, so Cancel works even mid-hash of a very large file; the scan manifest is posted in batches of 5,000 entries to stay under the server's 8 MiB scan body cap; a failed chunk is retried up to 3 times with exponential backoff plus jitter (capped at 8 s).

Production Deployment

filedrop is a transfer component, not a standalone site. In production:

  • Always implement your own RequestHook — never deploy stock filedrop. The default binary's PassthroughHook performs no authentication; production runs your own binary built against the filedrop crate (see Custom Deployments).
  • Expose only the /files API (plus /healthz, /readyz) through your Ingress / Gateway API routes. Do not expose / and /assets/* — the embedded UI is a debugging convenience. Shipping a product UI built on the /files API is fine, but that UI belongs to another service; it is not filedrop's component.
  • Same-origin, or configure CORS. The simplest setup routes the /files API under the same domain (or a subdomain) as the frontend that calls it, so no cross-origin request occurs. When the frontend lives on a different origin, set FILEDROP_CORS_ALLOW_ORIGINS (see Configuration) and filedrop applies CORS itself, preflight included — no ingress/gateway CORS rules needed. If a browser client must read filedrop's non-safelisted response headers (e.g. Upload-Offset, Content-Range for resumable uploads), add them to FILEDROP_CORS_EXPOSE_HEADERS. TLS and domain names remain the deployment's responsibility (terminate TLS at the ingress/gateway).
  • Quota lives in the storage backend, not in filedrop — size the PVCs / set filesystem quotas (e.g. CephFS quotas). When a write hits EDQUOT/ENOSPC, the server surfaces 507 Insufficient Storage.

Worked example: reaching any PVC (CephFS)

CSI drivers generally provision each PVC as a subpath of one shared filesystem, so this pattern is not CephFS-specific — CephFS is just the worked example here. Its CSI driver creates every PVC as a subvolume under a single filesystem root (e.g. /volumes/csi), which turns "one filedrop deployment serving every PVC in the cluster" into a pure path-mapping exercise:

  1. Mount the CephFS root as the filedrop root. Statically provision a PV that points at the CSI root (staticVolume: "true", rootPath: /volumes/csi), bind a PVC to it, and mount that PVC in the filedrop pod at the identical path, with FILEDROP_ROOT=/volumes/csi.
  2. Resolve the target PVC to a root-relative path in your RequestHook. Given a PVC (e.g. named by a query parameter), follow PVC → bound PV → spec.csi.volumeAttributes.subvolumePath (a path like /volumes/csi/csi-vol-<uuid>/…), strip the root prefix, and return the remainder joined with the request path. Because the root is mounted at the identical path, no further translation is needed — and watch-backed caches (informers) make the lookup a pure in-memory operation inside the synchronous hook.

Browser uploads then land directly inside the PVC that training/inference pods mount. Quota still comes free: CephFS enforces each PVC's size on its subvolume regardless of which mount the write arrives through, so exhaustion surfaces as 507 Insufficient Storage.

Known Limitations

  • Browser SHA-256: hashing uses a vendored hash-wasm streaming SHA-256 (ui/assets/hash-wasm.js, no CDN), reading the file via file.stream() so memory stays O(1) and files > 4 GB hash without loading the whole file. It also works over plain HTTP (no secure-context requirement).
  • Browser-side download buffering: the built-in UI's streaming download accumulates chunks in memory before triggering the save, so very large browser downloads are memory-bound. External tools (curl/wget/aria2c) stream to disk and have no such limit.
  • Chunk size: configurable via FILEDROP_CHUNK_SIZE (default 2 MiB). The built-in frontend fetches it from GET /config before uploading so frontend and backend stay in sync.
  • Hooks are compiled in: swapping the RequestHook means rebuilding and redeploying the binary (by design — no dynamic loading).
  • TLS recommended: hashing no longer needs a secure context, but production should still terminate TLS at the ingress/gateway for transport security.
  • The storage root must be private to filedrop: cross-replica coordination (the mtime lease, the SHA-256 midstate in .meta sidecars) trusts the shared filesystem as-is. A process that writes .filedrop_tmp directly can forge staged bytes that still pass final SHA-256 verification via the midstate. Mount the root only into filedrop replicas; never share it with other writers — verification guards against transport corruption, not hostile co-writers on the same filesystem.

License

Licensed under either of

at your option.

Vendored third-party content (the ui/DESIGN.md design reference and ui/assets/hash-wasm.js) is listed in THIRD-PARTY-NOTICES.md.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in filedrop by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0