@stacksjs/storage 0.75.226 → 0.75.230

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,6 +76,34 @@ platform's final-symlink error. The function and its types are also exported
76
76
  from `@stacksjs/storage`. Existing `readTextFile` and disk readers keep their
77
77
  current behavior.
78
78
 
79
+ ## Completed local artifacts
80
+
81
+ Publish a bounded set of files into a new private directory. The completion
82
+ manifest becomes visible after data and directory sync barriers:
83
+
84
+ ```ts
85
+ import { writeCompletedArtifact } from '@stacksjs/storage/completed-artifact-directory'
86
+
87
+ const artifact = await writeCompletedArtifact(outputDirectory, [
88
+ { path: 'data/surface.asc', contents: numericSurface },
89
+ { path: 'report.json', contents: reportJson },
90
+ ], manifestJson)
91
+ ```
92
+
93
+ The manifest must be a JSON object. The function validates relative entry paths,
94
+ case-insensitive duplicates, file/directory collisions, shared buffers and size
95
+ limits before filesystem mutation. It copies byte arrays, including Buffer views,
96
+ before awaiting IO. Files are exclusive 0600, directories 0700, and existing
97
+ destinations are preserved. Limits are 1,024 files, 128 MiB total and a 16 MiB
98
+ manifest. Optional `{ directories: ['images'] }` creates empty directories.
99
+
100
+ Every file and directory entry receives fsync barriers before a synced pending
101
+ manifest receives the exclusive `manifest.json` hard-link name. A partial output
102
+ is retained for diagnosis. Parents must be operator controlled; callers own
103
+ authorization and domain-schema validation. This is a local-filesystem protocol
104
+ requiring hard links and directory fsync, with no storage hardware power-loss
105
+ guarantee. It is not an S3/disk-adapter transaction or content authentication.
106
+
79
107
  ## 🧪 Testing
80
108
 
81
109
  ```bash
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Publish a new private local artifact after syncing its files and directory entries.
3
+ * Parents must be operator controlled. Requires hard links and directory fsync;
4
+ * this protocol is not a tested storage hardware power-loss guarantee.
5
+ */
6
+ export declare function writeCompletedArtifact(directory: string, entries: readonly CompletedArtifactEntry[], manifestJson: string, options?: { directories?: readonly string[] }): Promise<{ directory: string, manifest: string }>;
7
+ export declare const completedArtifactLimits: unknown;
8
+ export declare interface CompletedArtifactEntry { path: string, contents: string | Uint8Array }
@@ -0,0 +1 @@
1
+ import{link,mkdir,open,unlink}from"node:fs/promises";import{dirname,join,resolve}from"node:path";import{Buffer}from"node:buffer";export const completedArtifactLimits=Object.freeze({files:1024,bytes:134217728,manifestBytes:16777216,depth:4});export async function writeCompletedArtifact(directory,entries,manifestJson,options={}){if(typeof directory!=="string"||directory.length<1||directory.length>4096||directory.includes("\x00"))throw TypeError("Invalid artifact directory");if(!Array.isArray(entries)||entries.length>completedArtifactLimits.files||typeof manifestJson!=="string"||manifestJson.length>completedArtifactLimits.manifestBytes)throw RangeError("Artifact resource limit");const manifestSize=Buffer.byteLength(manifestJson,"utf8");if(manifestSize>completedArtifactLimits.manifestBytes)throw RangeError("Completion manifest byte limit");let manifest;try{manifest=JSON.parse(manifestJson)}catch{throw TypeError("Invalid completion manifest JSON")}if(!manifest||typeof manifest!=="object"||Array.isArray(manifest))throw TypeError("Completion manifest must be a JSON object");let total=manifestSize;const paths=new Set(["manifest.json","manifest.pending.json"]);function validatePath(path){if(typeof path!=="string"||path.length>240||!/^[a-z0-9._/-]+$/i.test(path))throw TypeError("Invalid artifact entry path");const parts=path.split("/");if(parts.length>completedArtifactLimits.depth||parts.some((part)=>!part||part==="."||part===".."||part.length>128))throw TypeError("Unsafe artifact entry path")}const files=entries.map((entry)=>{if(!entry||typeof entry!=="object"||Object.keys(entry).length!==2||!Object.hasOwn(entry,"path")||!Object.hasOwn(entry,"contents"))throw TypeError("Invalid artifact entry");validatePath(entry.path);const key=entry.path.toLowerCase();if(paths.has(key))throw TypeError("Duplicate or reserved artifact path");paths.add(key);let size;if(typeof entry.contents==="string")size=Buffer.byteLength(entry.contents,"utf8");else if(entry.contents instanceof Uint8Array&&!(typeof SharedArrayBuffer<"u"&&entry.contents.buffer instanceof SharedArrayBuffer))size=entry.contents.length;else throw TypeError("Artifact contents must be text or unshared bytes");total+=size;if(total>completedArtifactLimits.bytes)throw RangeError("Artifact byte limit");return{path:entry.path,contents:entry.contents}});if(!options||typeof options!=="object"||Object.keys(options).some((key)=>key!=="directories")||options.directories!==void 0&&(!Array.isArray(options.directories)||options.directories.length>completedArtifactLimits.files))throw TypeError("Invalid artifact directory options");const directorySpellings=new Map;function registerDirectories(path,includeLeaf){const parts=path.split("/");for(let depth=1;depth<=parts.length-(includeLeaf?0:1);depth++){const parent=parts.slice(0,depth).join("/"),key=parent.toLowerCase();if(paths.has(key))throw TypeError("Artifact directory and file collision");const spelling=directorySpellings.get(key);if(spelling!==void 0&&spelling!==parent)throw TypeError("Artifact directory case alias collision");directorySpellings.set(key,parent)}}for(const path of options.directories??[]){validatePath(path);registerDirectories(path,!0)}for(const file of files)registerDirectories(file.path,!1);const manifestBytes=new TextEncoder().encode(manifestJson),snapshot=files.map((file)=>({path:file.path,bytes:typeof file.contents==="string"?new TextEncoder().encode(file.contents):new Uint8Array(file.contents)})),target=resolve(directory),directories=[...directorySpellings.values()];async function writeSynced(path,data){const file=await open(path,"wx",384);try{await file.writeFile(data);await file.sync()}finally{await file.close()}}async function syncDirectory(path){const folder=await open(path,"r");try{await folder.sync()}finally{await folder.close()}}await mkdir(target,{mode:448});for(const path of[...directories].sort((a,b)=>a.split("/").length-b.split("/").length))await mkdir(join(target,path),{mode:448});for(const file of snapshot)await writeSynced(join(target,file.path),file.bytes);for(const path of[...directories].sort((a,b)=>b.split("/").length-a.split("/").length))await syncDirectory(join(target,path));await syncDirectory(target);await syncDirectory(dirname(target));const pending=join(target,"manifest.pending.json"),completed=join(target,"manifest.json");await writeSynced(pending,manifestBytes);await link(pending,completed);await syncDirectory(target);await unlink(pending);await syncDirectory(target);return{directory:target,manifest:completed}}
package/dist/index.d.ts CHANGED
@@ -22,6 +22,7 @@ export * from './copy';
22
22
  export * from './delete';
23
23
  export * from './files';
24
24
  export * from './bounded-text-file';
25
+ export * from './completed-artifact-directory';
25
26
  export * from './folders';
26
27
  export * from './fs';
27
28
  export * from './temporary-workspaces';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./copy";export*from"./delete";export*from"./files";export*from"./bounded-text-file";export*from"./folders";export*from"./fs";export*from"./temporary-workspaces";export*from"./glob";export*from"./hash";export*from"./helpers";export* as storage from"./storage";export*from"./zip";export{ensureBucket,ensureConfiguredBuckets}from"./provision";export*from"./adapters";export*from"./types";export*from"./drivers";export{serveFile}from"./static-serve";export{Storage,StorageManager}from"./facade";export{UploadedFile,uploadedFile,uploadedFiles}from"./uploaded-file";export{azureDisk,backblazeDisk,configFromEnv,filebaseDisk,gcsDisk,hetznerDisk,localDisk,r2Disk,s3Disk}from"./types/filesystem";export{clearRevokedSignedStorageTokens,createSignedStorageToken,isSignedStorageTokenRevoked,revokeSignedStorageToken,verifySignedStorageToken}from"./signed-url";export{parseDiskPath,PathSanitizeError,sanitizePresignedDir,sanitizePresignedFilename}from"./path-sanitize";export{fileInfo,fileInfoFromBytes,fileKind,isPreviewable,resolveMime}from"./file-info";export{detectMimeFromMagicBytes,verifyUploadedMime}from"./mime-verify";export{signS3PresignedPost}from"./s3-presigned-post";
1
+ export*from"./copy";export*from"./delete";export*from"./files";export*from"./bounded-text-file";export*from"./completed-artifact-directory";export*from"./folders";export*from"./fs";export*from"./temporary-workspaces";export*from"./glob";export*from"./hash";export*from"./helpers";export* as storage from"./storage";export*from"./zip";export{ensureBucket,ensureConfiguredBuckets}from"./provision";export*from"./adapters";export*from"./types";export*from"./drivers";export{serveFile}from"./static-serve";export{Storage,StorageManager}from"./facade";export{UploadedFile,uploadedFile,uploadedFiles}from"./uploaded-file";export{azureDisk,backblazeDisk,configFromEnv,filebaseDisk,gcsDisk,hetznerDisk,localDisk,r2Disk,s3Disk}from"./types/filesystem";export{clearRevokedSignedStorageTokens,createSignedStorageToken,isSignedStorageTokenRevoked,revokeSignedStorageToken,verifySignedStorageToken}from"./signed-url";export{parseDiskPath,PathSanitizeError,sanitizePresignedDir,sanitizePresignedFilename}from"./path-sanitize";export{fileInfo,fileInfoFromBytes,fileKind,isPreviewable,resolveMime}from"./file-info";export{detectMimeFromMagicBytes,verifyUploadedMime}from"./mime-verify";export{signS3PresignedPost}from"./s3-presigned-post";
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/storage",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.226",
5
+ "version": "0.75.230",
6
6
  "description": "The Stacks file system.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -61,20 +61,20 @@
61
61
  "prepublishOnly": "bun run build"
62
62
  },
63
63
  "dependencies": {
64
- "@stacksjs/arrays": "0.75.226",
65
- "@stacksjs/cli": "0.75.226",
66
- "@stacksjs/config": "0.75.226",
67
- "@stacksjs/env": "0.75.226",
68
- "@stacksjs/error-handling": "0.75.226",
69
- "@stacksjs/logging": "0.75.226",
70
- "@stacksjs/path": "0.75.226",
71
- "@stacksjs/strings": "0.75.226",
64
+ "@stacksjs/arrays": "0.75.230",
65
+ "@stacksjs/cli": "0.75.230",
66
+ "@stacksjs/config": "0.75.230",
67
+ "@stacksjs/env": "0.75.230",
68
+ "@stacksjs/error-handling": "0.75.230",
69
+ "@stacksjs/logging": "0.75.230",
70
+ "@stacksjs/path": "0.75.230",
71
+ "@stacksjs/strings": "0.75.230",
72
72
  "@stacksjs/ts-cloud": "^0.16.58",
73
73
  "@stacksjs/ts-xml": "^0.1.0",
74
74
  "ts-images": "^0.2.23"
75
75
  },
76
76
  "devDependencies": {
77
- "@stacksjs/types": "0.75.226",
77
+ "@stacksjs/types": "0.75.230",
78
78
  "better-dx": "^0.2.24"
79
79
  }
80
80
  }