Features · Install · Language status · Usage · Platform
A file abstraction that trusts the bytes, not the extension. Built for backends that take uploads from the open internet: magic-byte MIME detection, size and content validation, and presigned S3 uploads — one
FileAPI over local files, URLs, streams, S3 objects, and browser uploads, ported natively to five languages: TypeScript, Python, Rust, Go, and .NET.
| Capability | What you get | |
|---|---|---|
| 🔒 | Trust the bytes | Magic-byte MIME detection catches spoofed uploads, in all five ports |
| ☁️ | S3 in one call | Upload, download, signed URLs, presigned uploads with size caps |
| 🌐 | One API, many sources | Local · URL · bytes · stream · S3 · multipart, one File type |
| 🌊 | Lazy streaming | Streams ingest without full buffering — all five ports |
| 📝 | Rich metadata | Detected MIME, size, timestamps, hash — on one object |
Magic-byte MIME detection catches spoofed uploads. A .php renamed to avatar.png fails validation because the bytes disagree with the claim.
- Magic-byte detection across 100+ file types — in every port
- Validation fails with a typed error when the client-claimed MIME disagrees with the bytes, when the file is oversize, or when the type isn't allowed
- One
validate()call; the errors map cleanly to HTTP 400
The error shape is idiomatic per language: TypeScript and Python throw FileContentMismatchError / FileSizeError / FileMimeError (all extending FileValidationError); .NET throws the same three as exceptions; Rust has a FileValidationError enum; Go returns one FileValidationError with a Kind field. Because catch-by-type doesn't survive that, all five also carry the same kind string (size · mime · content_mismatch) — the discriminant portable code branches on.
- Stream a file into S3 and pull S3 objects back through the same validation pipeline — all five ports
- Presigned upload URLs with
maxSizebaked into the signature, so oversized uploads are rejected by S3 before they hit you — all five ports - Signed download URLs — all five ports
- In .NET, S3 support is the separate
SmooAI.File.S3package, so .NET consumers who only need detection/validation skip the AWS SDK dependency (the TypeScript package currently ships its AWS SDK dependencies unconditionally)
Local filesystem, URL download, S3 object, raw bytes, streams, multipart form uploads, or a browser File/Blob — all resolve to the same File instance with the same validation and metadata surface.
All five ports ingest streams lazily — only a 64 KiB head is read for MIME sniffing, and the rest of the bytes flow through without buffering the whole file (createFromStreamLazy · from_stream(lazy=True) · from_stream_lazy · NewFromStreamLazy · CreateFromStreamLazyAsync). The semantics are identical because all five test suites load one shared fixture, spec/lazy-stream-contract.json — head size, what stays lazy, what triggers a full read, and what a read after a full iteration does. The honest per-port breakdown is in the capability matrix.
File name, real (detected) MIME type, size, created/modified timestamps, hash/checksum, source type — all on one object.
%%{init: {'theme':'base','themeVariables':{
'background':'#020618','primaryColor':'#0b1426','primaryTextColor':'#e6edf6','primaryBorderColor':'#2b3a52',
'lineColor':'#7c8aa0','secondaryColor':'#0b1426','tertiaryColor':'#0b1426','fontFamily':'ui-sans-serif, system-ui, sans-serif',
'clusterBkg':'#0b1426','clusterBorder':'#22304a'}}}%%
flowchart LR
SRC["local file · URL · bytes<br/>stream · S3 · multipart · Blob"] --> F
subgraph F["File"]
D["magic-byte MIME detection"] --> V["validate()<br/>size · allowed types · claim vs bytes"]
V --> M["metadata<br/>name · MIME · size · hash"]
end
F --> OUT["save · S3 upload · FormData<br/>base64 · signed URLs"]
classDef warm fill:#f49f0a,stroke:#ff6b6c,color:#1a0f00;
classDef teal fill:#00a6a6,stroke:#00c2c2,color:#011;
class V warm
class SRC,OUT teal
| Language | Package | Install |
|---|---|---|
| TypeScript | @smooai/file |
pnpm add @smooai/file |
| Python | smooai-file |
pip install smooai-file |
| Rust | smooai-file |
cargo add smooai-file |
| Go | github.com/SmooAI/file/go/file/v2 |
go get github.com/SmooAI/file/go/file/v2 |
| .NET (core) | SmooAI.File |
dotnet add package SmooAI.File |
| .NET (S3) | SmooAI.File.S3 |
dotnet add package SmooAI.File.S3 |
Language-specific source lives in src/ (TypeScript), python/, rust/, go/, and dotnet/. The .NET port uses Mime-Detective for magic-byte MIME sniffing.
Every port carries the core promise: magic-byte MIME detection, typed size/mime/content-mismatch validation, rich metadata, S3 upload + presigned/signed URLs, and creation from local files, URLs, bytes, streams, S3, and multipart uploads. Beyond that the surfaces are uneven — here is the verified breakdown, so you know before you pick one:
| Capability | TypeScript | Python | Rust | Go | .NET |
|---|---|---|---|---|---|
Magic-byte detection + typed validate() |
✅ | ✅ | ✅ | ✅ | ✅ |
| Lazy streaming ingest | ✅ | ✅ | ✅ | ✅ | ✅ |
Chunked reads (iter_bytes) |
✅ | ✅ | ✅ | ✅ | ✅ (OpenReadStream) |
pipeTo a writable stream |
✅ | ❌ | ❌ | ❌ | ❌ |
append / prepend / truncate |
✅ | ✅ | ❌ | ✅ | ❌ |
exists / isReadable / isWritable / getStats |
✅ | ✅ | ❌ | ❌ | ❌ |
| Upload to S3 + presigned upload URL | ✅ | ✅ | ✅ | ✅ | ✅ (S3 pkg) |
| Signed download URL | ✅ | ✅ | ✅ | ✅ | ✅ |
saveToS3 / moveToS3 (returns new File) |
✅ | ✅ | ❌ | ❌ | ❌ |
downloadFromS3 (S3 → local path) |
❌ | ✅ | ❌ | ❌ | ❌ |
setMetadata |
✅ | ✅ | ✅ | ✅ | ✅ |
On
downloadFromS3: only Python writes an S3 object to a local path. Rust'sdownload_from_s3is a plain alias forfrom_s3, and Go's replaces the receiver in place — neither touches the filesystem, so neither belongs in this row. TypeScript composes the same thing today with(await File.createFromS3(b, k)).saveToFile(p).
Same semantics where a capability exists in two ports; each port is written idiomatically for its ecosystem and carries its own test suite.
Errors are portable by kind, not by type. TypeScript, Python and .NET raise three distinct classes; Rust collapses them into one enum; Go returns one struct with a Kind field — so catch (e) { if (e instanceof FileSizeError) } has no equivalent in Rust or Go. Every port now also carries the same kind discriminant ("size" · "mime" · "content_mismatch"), and that is what portable code branches on. The values and the fields each one carries are pinned by spec/error-taxonomy.json, which all five test suites load.
TypeScript below; the same shapes exist per the matrix above in Python, Rust, Go, and .NET.
Jump to a pattern:
- Basic usage
- Reading and saving
- S3 integration
- File type detection
- FormData support
- Web File / Blob (Hono, Next.js, Browser)
- Validation (size, mime, content-vs-claim)
- Base64 encoding
- Presigned upload URL
import File from '@smooai/file';
// Create a file from a local path
const file = await File.createFromFile('path/to/file.txt');
// Read file contents
const content = await file.readFileString();
console.log(content);
// Get file metadata
console.log(file.metadata);
// {
// name: 'file.txt',
// mimeType: 'text/plain',
// size: 1234,
// extension: 'txt',
// path: 'path/to/file.txt',
// lastModified: Date,
// createdAt: Date
// }import File from '@smooai/file';
// Create a file from a URL
const file = await File.createFromUrl('https://example.com/file.zip');
// Pipe to a destination stream
await file.pipeTo(someWritableStream);
// Read as bytes (note: buffers the content — the TS port has no lazy streaming yet)
const bytes = await file.readFileBytes();
// Save to filesystem
const { original, newFile } = await file.saveToFile('downloads/file.zip');import File from '@smooai/file';
// Create from S3
const file = await File.createFromS3('my-bucket', 'path/to/file.jpg');
// Upload to S3
await file.uploadToS3('my-bucket', 'remote/file.jpg');
// Save to S3 (creates new file instance)
const { original, newFile } = await file.saveToS3('my-bucket', 'remote/file.jpg');
// Move to S3 (deletes local file if source was local)
const s3File = await file.moveToS3('my-bucket', 'remote/file.jpg');
// Generate signed URL
const signedUrl = await s3File.getSignedUrl(3600); // URL expires in 1 hourimport File from '@smooai/file';
const file = await File.createFromFile('document.xml');
// Get file type information (detected via magic numbers)
console.log(file.mimeType); // 'application/xml'
console.log(file.extension); // 'xml'
// File type is automatically detected from:
// - Magic numbers (via file-type)
// - MIME type headers
// - File extension
// - Custom detectorsimport File from '@smooai/file';
const file = await File.createFromFile('document.pdf');
// Convert to FormData for uploads
const formData = await file.toFormData('document');
// Use with fetch or other HTTP clients
await fetch('https://api.example.com/upload', {
method: 'POST',
body: formData,
});import File from '@smooai/file';
// Hono multipart route
app.post('/upload', async (c) => {
const form = await c.req.formData();
const webFile = form.get('file') as globalThis.File;
// Preserves the web File's name and type hints.
const file = await File.createFromWebFile(webFile);
// …validate, upload, etc.
});import File, { FileValidationError } from '@smooai/file';
const file = await File.createFromWebFile(webFile);
try {
await file.validate({
maxSize: 5 * 1024 * 1024, // 5MB
allowedMimes: ['image/png', 'image/jpeg', 'image/webp'],
expectedMimeType: webFile.type, // compares magic-byte detection vs claimed Content-Type
});
} catch (err) {
if (err instanceof FileValidationError) {
// FileSizeError | FileMimeError | FileContentMismatchError — map to HTTP 400
throw new HTTPException(400, { message: err.message });
}
throw err;
}expectedMimeType is the primary defense against mime-spoofing: a .php file uploaded with Content-Type: image/png will fail because magic-byte detection doesn't match the claim.
import File from '@smooai/file';
const file = await File.createFromUrl('https://s3.example.com/invoice.pdf');
await sendEmail({
attachments: [
{
filename: 'invoice.pdf',
content: await file.toBase64(),
encoding: 'base64',
},
],
});import File from '@smooai/file';
// Server issues a time-limited signed URL the client uploads bytes to directly.
// `maxSize` is baked into the signature so oversized uploads are rejected by S3.
const url = await File.createPresignedUploadUrl({
bucket: Resource.Bucket.name,
key: `avatars/${userId}.png`,
contentType: 'image/png',
expiresIn: 600,
maxSize: 2 * 1024 * 1024,
});- TypeScript · Node.js File System API · AWS SDK v3
- file-type for magic number-based MIME type detection (TypeScript port)
- Mime-Detective for MIME sniffing (.NET port)
- @smooai/fetch for URL downloads
- @smooai/logger for structured logging
@smooai/file is built and open-sourced by Smoo AI — the AI-powered business platform with AI built into every product: CRM, customer support, campaigns, field service, observability, and developer tools.
- 🧰 More open source from Smoo AI — smoo.ai/open-source
- 🧩 Sibling packages — @smooai/fetch, @smooai/logger, @smooai/config, smooth-operator, smooth
Contributions are welcome. This project uses changesets to manage versions and releases.
-
Fork the repository
-
Create your branch (
git checkout -b amazing-feature) -
Make your changes (the five ports live in
src/,python/,rust/,go/,dotnet/) -
Add a changeset to document them:
pnpm changeset
You'll be prompted to choose a version bump (patch, minor, or major) and describe the change.
-
Commit your changes (
git commit -m 'Add some amazing feature') -
Push to the branch (
git push origin feature/amazing-feature) -
Open a pull request — reference any related issues in the description
The maintainers will review your PR and may request changes before merging.
MIT — see LICENSE.
Brent Rager
Smoo GitHub: https://github.com/SmooAI
Built by Smoo AI — AI built into every product.
