Skip to content

Commit

Permalink
Use web crypto API instead of node:crypto
Browse files Browse the repository at this point in the history
  • Loading branch information
mjackson committed Jan 8, 2025
1 parent a5b04fd commit b184489
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 15 deletions.
2 changes: 1 addition & 1 deletion packages/file-storage/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is the changelog for [`file-storage`](https://github.com/mjackson/remix-the-web/tree/main/packages/file-storage). It follows [semantic versioning](https://semver.org/).

## v0.3.1
## HEAD

- Fixes race conditions with concurrent calls to `set`
- Shards storage directories for more scalable file systems
Expand Down
35 changes: 21 additions & 14 deletions packages/file-storage/src/lib/local-file-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import * as fs from 'node:fs';
import * as fsp from 'node:fs/promises';
import * as path from 'node:path';
import { openFile, writeFile } from '@mjackson/lazy-file/fs';
import * as crypto from 'node:crypto';

import { type FileStorage } from './file-storage.ts';

interface FileMetadata {
name: string;
type: string;
mtime: number;
}

/**
* A `FileStorage` that is backed by the local filesystem.
*
Expand Down Expand Up @@ -39,7 +44,8 @@ export class LocalFileStorage implements FileStorage {
}

async has(key: string): Promise<boolean> {
let { metaPath } = this.#getFilePaths(key);
let { metaPath } = await this.#getPaths(key);

try {
await fsp.access(metaPath);
return true;
Expand All @@ -52,13 +58,12 @@ export class LocalFileStorage implements FileStorage {
// Remove any existing file with the same key.
await this.remove(key);

let { directory, filePath, metaPath } = this.#getFilePaths(key);
let { directory, filePath, metaPath } = await this.#getPaths(key);

// Ensure directory exists
await fsp.mkdir(directory, { recursive: true });

let handle = await fsp.open(filePath, 'w');
await writeFile(handle, file);
await writeFile(filePath, file);

let metadata: FileMetadata = {
name: file.name,
Expand All @@ -69,7 +74,7 @@ export class LocalFileStorage implements FileStorage {
}

async get(key: string): Promise<File | null> {
let { filePath, metaPath } = this.#getFilePaths(key);
let { filePath, metaPath } = await this.#getPaths(key);

try {
let metadataContent = await fsp.readFile(metaPath, 'utf-8');
Expand All @@ -84,12 +89,13 @@ export class LocalFileStorage implements FileStorage {
if (!isNoEntityError(error)) {
throw error;
}

return null;
}
}

async remove(key: string): Promise<void> {
let { filePath, metaPath } = this.#getFilePaths(key);
let { filePath, metaPath } = await this.#getPaths(key);

try {
await Promise.all([fsp.unlink(filePath), fsp.unlink(metaPath)]);
Expand All @@ -100,11 +106,11 @@ export class LocalFileStorage implements FileStorage {
}
}

#getFilePaths(key: string): { directory: string; filePath: string; metaPath: string } {
let hash = crypto.createHash('sha256').update(key).digest('hex');
async #getPaths(key: string): Promise<{ directory: string; filePath: string; metaPath: string }> {
let hash = await computeHash(key);
let shardDir = hash.slice(0, 8);
let directory = path.join(this.#dirname, shardDir);
let filename = `${hash}.bin`;
let filename = `${hash.slice(8)}.bin`;
let metaname = `${hash}.meta.json`;

return {
Expand All @@ -115,10 +121,11 @@ export class LocalFileStorage implements FileStorage {
}
}

interface FileMetadata {
name: string;
type: string;
mtime: number;
async function computeHash(key: string, algorithm = 'SHA-256'): Promise<string> {
let digest = await crypto.subtle.digest(algorithm, new TextEncoder().encode(key));
return Array.from(new Uint8Array(digest))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}

function isNoEntityError(obj: unknown): obj is NodeJS.ErrnoException & { code: 'ENOENT' } {
Expand Down

0 comments on commit b184489

Please sign in to comment.