@stacksjs/storage 0.75.204 → 0.75.207

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/dist/index.d.ts CHANGED
@@ -23,6 +23,7 @@ export * from './delete';
23
23
  export * from './files';
24
24
  export * from './folders';
25
25
  export * from './fs';
26
+ export * from './temporary-workspaces';
26
27
  export * from './glob';
27
28
  export * from './hash';
28
29
  export * from './helpers';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./copy";export*from"./delete";export*from"./files";export*from"./folders";export*from"./fs";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"./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";
@@ -0,0 +1,38 @@
1
+ import process from 'node:process';
2
+ /** Report missing inspection permission explicitly; never infer that an unreadable process is idle. */
3
+ export declare function inspectTemporaryWorkspace(path: string): TemporaryWorkspaceInspection;
4
+ export declare function temporaryWorkspaceIsUnused(path: string): boolean;
5
+ export declare interface TemporaryWorkspaceRecord {
6
+ readonly id: string
7
+ readonly key: string
8
+ readonly path: string
9
+ readonly inode: string
10
+ readonly device: string
11
+ readonly uid: string
12
+ }
13
+ export declare interface TemporaryWorkspaceInspection {
14
+ unused: boolean
15
+ reason: 'unused' | 'mounted' | 'process-reference' | 'inspection-unavailable' | 'unsupported-platform'
16
+ pid?: number
17
+ operation?: string
18
+ code?: string
19
+ }
20
+ export declare interface TemporaryWorkspaceRetirementInspection {
21
+ ready: boolean
22
+ reason: 'ready' | 'record-invalid' | 'not-authorized' | 'identity-changed' | TemporaryWorkspaceInspection['reason']
23
+ record?: TemporaryWorkspaceRecord
24
+ usage?: TemporaryWorkspaceInspection
25
+ }
26
+ /** Recoverable directories with separate live ownership and crash-recovery lifecycles. */
27
+ export declare class TemporaryWorkspaceJournal {
28
+ readonly directory: string;
29
+ readonly temporaryRoot: string;
30
+ readonly prefix: string;
31
+ readonly ownerUid: number;
32
+ constructor(options: { directory: string, temporaryRoot: string, prefix: string, ownerUid?: number });
33
+ create(key: string): TemporaryWorkspaceRecord;
34
+ list(): TemporaryWorkspaceRecord[];
35
+ complete(record: TemporaryWorkspaceRecord): boolean;
36
+ retire(id: string, mayRetire: (record: TemporaryWorkspaceRecord) => boolean | Promise<boolean>): Promise<boolean>;
37
+ inspectRetirement(id: string, mayRetire: (record: TemporaryWorkspaceRecord) => boolean | Promise<boolean>): Promise<TemporaryWorkspaceRetirementInspection>;
38
+ }
@@ -0,0 +1,4 @@
1
+ import{existsSync,lstatSync,mkdirSync,mkdtempSync,readFileSync,readdirSync,readlinkSync,realpathSync,renameSync,rmSync,writeFileSync}from"node:fs";import{basename,dirname,join,resolve}from"node:path";import process from"node:process";function vanished(error){return error instanceof Error&&"code"in error&&(error.code==="ENOENT"||error.code==="ESRCH")}function beneath(path,root){const clean=path.replace(/ \(deleted\)$/,"");return clean===root||clean.startsWith(`${root}/`)}function mountedWorkspace(table,path){return table.split(`
2
+ `).some((line)=>{const fields=line.split(" ");return[fields[3],fields[4]].some((field)=>field&&beneath(field.replace(/\\([0-7]{3})/g,(_match,octal)=>String.fromCharCode(Number.parseInt(octal,8))),path))})}export function inspectTemporaryWorkspace(path){let operation="mounts";try{if(process.platform==="linux"){if(mountedWorkspace(readFileSync("/proc/self/mountinfo","utf8"),path))return{unused:!1,reason:"mounted"};const uid=process.getuid?.();for(const pid of readdirSync("/proc").filter((name)=>/^\d+$/.test(name))){const directory=`/proc/${pid}`;try{operation="process-owner";const identities=/^Uid:\s+(.+)$/m.exec(readFileSync(`${directory}/status`,"utf8"))?.[1]?.trim().split(/\s+/).map(Number);if(!identities?.length)return{unused:!1,reason:"inspection-unavailable",pid:Number(pid),operation};if(uid!==0&&!identities.includes(Number(uid)))continue;operation="process-mounts";if(mountedWorkspace(readFileSync(`${directory}/mountinfo`,"utf8"),path))return{unused:!1,reason:"mounted",pid:Number(pid)};operation="process-paths";const references=[readlinkSync(`${directory}/cwd`),readlinkSync(`${directory}/exe`)];operation="process-files";for(const fd of readdirSync(`${directory}/fd`))try{references.push(readlinkSync(`${directory}/fd/${fd}`))}catch(error){if(!vanished(error))throw error}operation="process-maps";for(const line of readFileSync(`${directory}/maps`,"utf8").split(`
3
+ `)){const mapped=/^\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(.+)$/.exec(line)?.[1];if(mapped)references.push(mapped)}if(references.some((reference)=>beneath(reference,path)))return{unused:!1,reason:"process-reference",pid:Number(pid)}}catch(error){if(!vanished(error))return{unused:!1,reason:"inspection-unavailable",pid:Number(pid),operation,code:error instanceof Error&&"code"in error?String(error.code):void 0}}}return{unused:!0,reason:"unused"}}if(process.platform==="darwin"){const mounts=Bun.spawnSync(["mount"],{stdout:"pipe",stderr:"pipe"});if(mounts.exitCode!==0)return{unused:!1,reason:"inspection-unavailable",operation};for(const line of mounts.stdout.toString().split(`
4
+ `)){const mount=/ on (.+) \(/.exec(line)?.[1];if(mount&&beneath(mount,path))return{unused:!1,reason:"mounted"}}const uid=process.getuid?.();if(uid===void 0)return{unused:!1,reason:"inspection-unavailable",operation:"process-owner"};operation="process-files";const files=Bun.spawnSync(["lsof","-nP","-F0n",...uid===0?[]:["-u",String(uid)]],{stdout:"pipe",stderr:"pipe"});if(files.exitCode!==0||files.stderr.length>0)return{unused:!1,reason:"inspection-unavailable",operation};const used=files.stdout.toString().split("\x00").some((field)=>field.startsWith("n")&&beneath(field.slice(1),path));return{unused:!used,reason:used?"process-reference":"unused"}}}catch(error){return{unused:!1,reason:"inspection-unavailable",operation,code:error instanceof Error&&"code"in error?String(error.code):void 0}}return{unused:!1,reason:"unsupported-platform"}}export function temporaryWorkspaceIsUnused(path){return inspectTemporaryWorkspace(path).unused}export class TemporaryWorkspaceJournal{directory;temporaryRoot;prefix;ownerUid;active=new Map;rootInode;rootDevice;journalInode;journalDevice;constructor(options){if(typeof options.prefix!=="string"||!/^[a-z0-9_-]+$/i.test(options.prefix))throw TypeError("Invalid temporary workspace prefix");const currentUid=process.getuid?.(),ownerUid=options.ownerUid??currentUid;if(currentUid===void 0||ownerUid===void 0||!Number.isSafeInteger(ownerUid)||ownerUid<0)throw Error("Workspace ownership requires a POSIX user identity");if(ownerUid!==currentUid&&currentUid!==0)throw Error("Only root may maintain another user's workspace journal");if(ownerUid!==currentUid&&!existsSync(options.directory))throw Error("Another user's journal must already exist");mkdirSync(options.directory,{recursive:!0,mode:448});const journal=lstatSync(options.directory);if(!journal.isDirectory()||journal.isSymbolicLink()||journal.uid!==ownerUid||(journal.mode&63)!==0)throw Error("A workspace journal must be a private directory owned by its declared user");this.ownerUid=ownerUid;this.directory=realpathSync(options.directory);this.temporaryRoot=realpathSync(options.temporaryRoot);if(this.directory===this.temporaryRoot)throw Error("The journal cannot be the temporary root");this.prefix=options.prefix;const root=lstatSync(this.temporaryRoot,{bigint:!0}),journalIdentity=lstatSync(this.directory,{bigint:!0});this.rootInode=String(root.ino);this.rootDevice=String(root.dev);this.journalInode=String(journalIdentity.ino);this.journalDevice=String(journalIdentity.dev)}create(key){if(typeof key!=="string"||!/^[a-z0-9_-]{1,80}$/i.test(key))throw TypeError("Invalid workspace key");if(this.ownerUid!==process.getuid?.())throw Error("Maintenance cannot create workspaces for another user");if(!this.rootsUnchanged())throw Error("The workspace or journal root changed");const path=mkdtempSync(join(this.temporaryRoot,`${this.prefix}${key}-`)),identity=lstatSync(path,{bigint:!0}),record=Object.freeze({id:crypto.randomUUID(),key,path,inode:String(identity.ino),device:String(identity.dev),uid:String(identity.uid)}),file=join(this.directory,`${record.id}.json`);try{writeFileSync(`${file}.tmp`,JSON.stringify(record),{flag:"wx",mode:384});renameSync(`${file}.tmp`,file);this.active.set(record.id,record);return record}catch(error){rmSync(path,{recursive:!0,force:!0});rmSync(`${file}.tmp`,{force:!0});throw error}}list(){if(!this.rootsUnchanged())return[];const records=[];for(const entry of readdirSync(this.directory).slice(0,1000)){if(!entry.endsWith(".json"))continue;const record=this.read(entry.slice(0,-5));if(record)records.push(record)}return records}complete(record){if(!record||this.active.get(record.id)!==record)return!1;if(!this.remove(record))return!1;this.active.delete(record.id);return!0}async retire(id,mayRetire){const verdict=await this.inspectRetirement(id,mayRetire);if(!verdict.ready||!verdict.record||!this.remove(verdict.record))return!1;this.active.delete(verdict.record.id);return!0}async inspectRetirement(id,mayRetire){const record=this.read(id);if(!record)return{ready:!1,reason:"record-invalid"};if(await mayRetire(record)!==!0)return{ready:!1,reason:"not-authorized",record};if(!this.rootsUnchanged())return{ready:!1,reason:"identity-changed",record};const identity=this.identity(record);if(identity==="changed"||identity==="unavailable")return{ready:!1,reason:"identity-changed",record};if(identity==="missing")return{ready:!0,reason:"ready",record};const usage=inspectTemporaryWorkspace(record.path);return{ready:usage.unused,reason:usage.unused?"ready":usage.reason,record,usage}}identity(record){try{const identity=lstatSync(record.path,{bigint:!0});return identity.isDirectory()&&!identity.isSymbolicLink()&&String(identity.uid)===record.uid&&String(identity.uid)===String(this.ownerUid)&&String(identity.ino)===record.inode&&String(identity.dev)===record.device?"same":"changed"}catch(error){return vanished(error)?"missing":"unavailable"}}remove(record){try{if(!this.rootsUnchanged())return!1;const identity=this.identity(record);if(identity==="changed"||identity==="unavailable")return!1;if(identity==="same")rmSync(record.path,{recursive:!0})}catch(error){if(!vanished(error)||this.identity(record)!=="missing")return!1}rmSync(join(this.directory,`${record.id}.json`),{force:!0});return!0}read(id){if(typeof id!=="string"||!/^[0-9a-f-]{36}$/.test(id))return null;if(!this.rootsUnchanged())return null;try{const file=join(this.directory,`${id}.json`),identity=lstatSync(file);if(!identity.isFile()||identity.isSymbolicLink()||identity.uid!==this.ownerUid||(identity.mode&63)!==0||identity.size>4096)return null;const value=JSON.parse(readFileSync(file,"utf8"));if(!value||value.id!==id||typeof value.key!=="string"||!/^[a-z0-9_-]{1,80}$/i.test(value.key)||typeof value.path!=="string"||resolve(value.path)!==value.path||dirname(value.path)!==this.temporaryRoot||!basename(value.path).startsWith(`${this.prefix}${value.key}-`)||!["inode","device","uid"].every((key)=>typeof value[key]==="string"&&/^\d+$/.test(value[key])))return null;return Object.freeze(value)}catch{return null}}rootsUnchanged(){try{const root=lstatSync(this.temporaryRoot,{bigint:!0}),journal=lstatSync(this.directory,{bigint:!0});return root.isDirectory()&&!root.isSymbolicLink()&&String(root.ino)===this.rootInode&&String(root.dev)===this.rootDevice&&journal.isDirectory()&&!journal.isSymbolicLink()&&String(journal.ino)===this.journalInode&&String(journal.dev)===this.journalDevice&&String(journal.uid)===String(this.ownerUid)&&(journal.mode&0o077n)===0n}catch{return!1}}}
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.204",
5
+ "version": "0.75.207",
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.204",
65
- "@stacksjs/cli": "0.75.204",
66
- "@stacksjs/config": "0.75.204",
67
- "@stacksjs/env": "0.75.204",
68
- "@stacksjs/error-handling": "0.75.204",
69
- "@stacksjs/logging": "0.75.204",
70
- "@stacksjs/path": "0.75.204",
71
- "@stacksjs/strings": "0.75.204",
72
- "@stacksjs/ts-cloud": "^0.16.56",
64
+ "@stacksjs/arrays": "0.75.207",
65
+ "@stacksjs/cli": "0.75.207",
66
+ "@stacksjs/config": "0.75.207",
67
+ "@stacksjs/env": "0.75.207",
68
+ "@stacksjs/error-handling": "0.75.207",
69
+ "@stacksjs/logging": "0.75.207",
70
+ "@stacksjs/path": "0.75.207",
71
+ "@stacksjs/strings": "0.75.207",
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.204",
77
+ "@stacksjs/types": "0.75.207",
78
78
  "better-dx": "^0.2.24"
79
79
  }
80
80
  }