@arraypress/storage-local
v1.0.0
Published
Local filesystem storage adapter for @arraypress/storage (Node.js, Docker, dev)
Maintainers
Readme
@arraypress/storage-local
Local filesystem storage adapter for @arraypress/storage. Stores files on disk with sidecar metadata. Designed for local development, Docker deployments, and self-hosted Node.js servers.
Works in Node.js only (requires node:fs).
Installation
npm install @arraypress/storage-local @arraypress/storageUsage
Local Development
import { createLocalStorage } from '@arraypress/storage-local';
const storage = createLocalStorage({
directory: './data/uploads',
publicUrl: 'http://localhost:3000/files',
});
// Upload
await storage.upload({
key: 'photos/sunset.jpg',
body: fileBuffer,
contentType: 'image/jpeg',
metadata: { originalName: 'IMG_1234.jpg' },
});
// Download
const file = await storage.download('photos/sunset.jpg');
if (file) {
console.log(file.contentType); // 'image/jpeg'
console.log(file.size); // bytes
}
// Check existence
await storage.exists('photos/sunset.jpg'); // true
// Delete
await storage.delete('photos/sunset.jpg');
// Public URL
storage.getPublicUrl('photos/sunset.jpg');
// => 'http://localhost:3000/files/photos/sunset.jpg'With Hono on Node.js
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { createLocalStorage } from '@arraypress/storage-local';
const storage = createLocalStorage({
directory: './data/uploads',
publicUrl: 'http://localhost:3000/files',
});
const app = new Hono();
app.post('/upload', async (c) => {
const body = await c.req.arrayBuffer();
const result = await storage.upload({
key: `uploads/${Date.now()}.jpg`,
body: new Uint8Array(body),
contentType: 'image/jpeg',
});
return c.json(result);
});
app.get('/files/*', async (c) => {
const key = c.req.path.replace('/files/', '');
const file = await storage.download(key);
if (!file) return c.notFound();
return new Response(file.body, {
headers: {
'Content-Type': file.contentType,
'Content-Length': String(file.size),
},
});
});
serve(app, { port: 3000 });Docker
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN mkdir -p /data/uploads
ENV STORAGE_DIR=/data/uploads
EXPOSE 3000
CMD ["node", "server.js"]# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- uploads:/data/uploads
volumes:
uploads:const storage = createLocalStorage({
directory: process.env.STORAGE_DIR || './data/uploads',
publicUrl: 'https://myapp.com/files',
});Content-Addressed Uploads
import { createLocalStorage } from '@arraypress/storage-local';
import { contentHash, contentAddressedKey } from '@arraypress/storage';
const storage = createLocalStorage({ directory: './data/uploads' });
const hash = await contentHash(fileBuffer);
const key = contentAddressedKey(hash, 'photo.jpg', 'media/');
if (!await storage.exists(key)) {
await storage.upload({ key, body: fileBuffer, contentType: 'image/jpeg' });
}How It Works
- Files are stored at
{directory}/{key}on disk - Content type and metadata are stored in a sidecar
.meta.jsonfile alongside each object - Nested keys (e.g.
photos/2024/sunset.jpg) create subdirectories automatically - Path traversal is prevented -- keys like
../../../etc/passwdthrow aPERMISSION_DENIEDerror delete()silently succeeds if the key does not exist (matches S3/R2 behaviour)getSignedDownloadUrl()andgetSignedUploadUrl()throwNOT_SUPPORTED-- serve files through your HTTP server insteadcreateMultipartUpload()throwsNOT_SUPPORTED-- upload the full file directly
API Reference
createLocalStorage(options): Storage
Creates a Storage adapter backed by the local filesystem.
Options
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| directory | string | Yes | Base directory for file storage. Created if it does not exist. |
| publicUrl | string | No | Public URL prefix for getPublicUrl() (e.g. 'http://localhost:3000/files') |
Re-exported from @arraypress/storage
StorageinterfaceStorageErrorclasscontentHash(),contentAddressedKey(),safeDisposition()helpers- All type definitions
License
MIT
