@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 +28 -0
- package/dist/completed-artifact-directory.d.ts +8 -0
- package/dist/completed-artifact-directory.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +10 -10
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.
|
|
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.
|
|
65
|
-
"@stacksjs/cli": "0.75.
|
|
66
|
-
"@stacksjs/config": "0.75.
|
|
67
|
-
"@stacksjs/env": "0.75.
|
|
68
|
-
"@stacksjs/error-handling": "0.75.
|
|
69
|
-
"@stacksjs/logging": "0.75.
|
|
70
|
-
"@stacksjs/path": "0.75.
|
|
71
|
-
"@stacksjs/strings": "0.75.
|
|
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.
|
|
77
|
+
"@stacksjs/types": "0.75.230",
|
|
78
78
|
"better-dx": "^0.2.24"
|
|
79
79
|
}
|
|
80
80
|
}
|