UNPKG

@produck/json-index-archive

Version:

7 lines (6 loc) 6.96 kB
/*! * @produck/json-index-archive v0.1.3 * (c) 2024-2025 ChaosLee * Released under the MIT License. */ "use strict";var t=require("node:fs"),e=require("node:path"),i=require("node:stream"),s=require("node:events"),r=require("@produck/ow"),n=require("@produck/idiom"),o=require("node:crypto");const a=Symbol("extension");class c{[a]=null;get extension(){return this[a]}constructor(t=null){this[a]=t}static isNode(t){return t instanceof this}}const l=Symbol("size"),d=Symbol("offset");class E extends c{[d]=0;[l]=0;constructor(t,e,...i){n.Assert.Integer(t,"offset"),n.Assert.Integer(e,"size"),t<0&&r.Error.Range("Offset MUST be > 0."),e<0&&r.Error.Range("Size MUST be > 0."),super(...i),this[d]=t,this[l]=e}get offset(){return this[d]}get size(){return this[l]}}const h=/[<>:"/\\|*?]+/;function u(t,e){const i=`sections[${e}]`;n.Assert.Type.String(t,i),h.test(t)&&r.Error.Common(`Bad section with illegal charact at [${e}].`)}function p(t,i="pathname"){n.Assert.Type.String(t,i),e.posix.isAbsolute(t)&&!t.includes(e.win32.sep)||r.Error.Common("Bad pathname, should be POSIX absolute path.")}const y={SEEK:1,DONE:2,ALL:0};y.ALL=y.SEEK|y.DONE;const m=Symbol("children"),S={DEFAULT:()=>0};class T extends c{[m]={};getChild(t){n.Assert.Type.String(t,"name");const e=this[m][t];return n.Is.Type.Undefined(e)?null:e}appendChild(t,e){n.Assert.Type.String(t,"name"),T.isNode(e)||E.isNode(e)||r.Invalid("node","FileNode | DirectoryNode"),null!==this.getChild(t)&&r.Error.Common(`Duplicated name "${t}".`),this[m][t]=e}*[Symbol.iterator](){const{[m]:t}=this;for(const e in t)yield Object.freeze([e,t[e]])}find(...t){if(0===t.length)return this;t.forEach(u);const e=this.getChild(t.shift());return 0===t.length?e:T.isNode(e)?e.find(...t):null}records(t=S.DEFAULT){return n.Assert.Type.Function(t,"order"),[...this].sort(t)}*children(t=1,...e){n.Assert.Integer(t,"depth");const i=function(t={}){n.Assert.Type.Object(t,"options");const e={visitAt:y.SEEK,order:S.DEFAULT},{visitAt:i=e.visitAt,order:s=e.order}=t;return n.Assert.Integer(i,"visitAt"),(i<0||i>y.ALL)&&r.Error.Range(`A "options.visitAt" MUST NOT be < 0 or > ${y.ALL}.`),n.Assert.Type.Function(s,"options.order"),e.visitAt=i,e.order=s,e}(...e);if(!(t<1))for(const e of this.records(i.order)){i.visitAt&y.SEEK&&(yield e);const s=e[1];T.isNode(s)&&(yield*s.children(t-1,i)),i.visitAt&y.DONE&&(yield e)}}}const f={FILE:0,DIRECTORY:1},F={TYPE:0,NAME:1,DIRECTORY:{CHILDREN:2,EXTEND:3},FILE:{OFFSET:2,SIZE:3,EXTEND:4}};var N={__proto__:null,NODE:F,TYPE:f,build:function*t(e,i){var s;n.Assert.Array(e,`DirectoryTuple[${F.DIRECTORY.CHILDREN}]`),s=i,E.isNode(s)||T.isNode(s)||r.Invalid("node","FileNode | DirectoryNode");for(const s of e){const e=s[F.TYPE],o=s[F.NAME];if(n.Assert.Type.String(o,`Tuple[${F.NAME}]`),e===f.FILE){const t=s[F.FILE.OFFSET],e=s[F.FILE.SIZE];n.Assert.Type.String(t,`FileTuple[${F.FILE.OFFSET}]`),n.Assert.Type.String(e,`FileTuple[${F.FILE.SIZE}]`);const a=Number(t);n.Is.NaN(a)&&r.Error.Common(`"FileTuple[${F.FILE.OFFSET}]" SHOULD NOT be NaN.`);const c=Number(e);n.Is.NaN(c)&&r.Error.Common(`"FileTuple[${F.FILE.SIZE}]" SHOULD NOT be NaN.`);const l=s[F.FILE.EXTEND],d=new E(a,c,l);i.appendChild(o,d),yield[...s]}if(e===f.DIRECTORY){const e=s[F.DIRECTORY.EXTEND],r=new T(e);i.appendChild(o,r),yield*t(s[F.DIRECTORY.CHILDREN],r)}}}};var I={__proto__:null,DEFAULT_BUFFER_BYTE_LENGTH:16384,FILE_SIZE_BUFFER_BYTE_LENGTH:8};const A=Symbol("pathname"),D=Symbol("root"),g=Symbol("archiveSize"),b=Symbol("fileSize"),{FILE_SIZE_BUFFER_BYTE_LENGTH:w}=I,L=Symbol("_sync"),R=Symbol("_exists"),O=Symbol("_open"),C=Symbol("_readdir"),_=Symbol("_readFile"),v=Symbol("_createReadStream"),Y=Symbol("_stat"),x=[()=>!0,()=>!1];async function U(e){await async function(e){return await t.promises.access(e).then(...x)}(e)||r.Error.Common(`File "${e}" is NOT existed.`)}class H{[D]=new T;[A]="";get pathname(){return this[A]}[g]=0n;get archiveSize(){return this[g]}[b]=0n;get fileSize(){return this[b]}get indexSize(){return Number(this[g])-w-Number(this[b])}exists(t){return p(t),this[R](t)}open(t){return p(t),this[O](t)}stat(t){return p(t),this[Y](t)}readdir(t,...e){return p(t),this[C](t,...e)}readFile(t,...e){return p(t),this[_](t,...e)}createReadStream(t,...e){return p(t),this[v](t,...e)}async sync(){await U(this[A]),await this[L]()}static async mount(t){n.Assert.Type.String(t,"pathname"),t=e.resolve(t),await U(t);const i=new this;return i[A]=t,await i.sync(),i}}async function P({readStream:t,piping:e}){const i=o.createHash("sha256");return t.on("data",(t=>i.update(t))),await e,i.digest("hex")}async function M({pathname:e}){const i=await t.promises.stat(e);return[Math.trunc(i.birthtimeMs)]}const $={withFileTypes:!0},z={autoClose:!1},{NODE:B,TYPE:X}=N,q=Symbol("fileHandlers"),Z=Symbol("directoryHandlers");function j(t,e){return t.isDirectory()&&e.isFile()?-1:t.isFile()&&e.isDirectory()?1:t.name.localeCompare(e.name)}function G(t){return t.isFile()||t.isDirectory()}const K=/^\.\./;async function*k(i){const s=(await t.promises.readdir(i,$)).filter(G).sort(j);for(const t of s){if(yield t,t.isDirectory()){const{parentPath:i,name:s}=t,r=e.join(i,s);yield*await k(r)}yield t}}exports.Archiver=class{#t="";constructor(t){this.#t=e.resolve(t)}async*entities(){const t=new Set;for await(const e of k(this.#t))t.has(e)||(t.add(e),yield e)}async*paths(){for await(const t of this.entities()){const i=e.relative(this.#t,t.parentPath),s=t.isDirectory()?e.sep:"";yield e.join(i,t.name)+s}}async*buildIndex(t=[]){const e=[null],i=[t];for await(const t of k(this.#t))if(t===e[0])e.shift(),t.isDirectory()&&i.shift();else{const s=[];if(s[B.NAME]=t.name,e.unshift(t),i[0].push(s),t.isFile()&&(s[B.TYPE]=X.FILE),t.isDirectory()){const t=[];s[B.TYPE]=X.DIRECTORY,s[B.DIRECTORY.CHILDREN]=t,i.unshift(t)}yield[t,s]}return i[0]}[q]=[M,P];[Z]=[M];async archive(o,a=438){n.Assert.Type.String(o,"destination"),n.Assert.Integer(a,"mode"),(a<0||a>511)&&r.Error.Range("A mode should >=0 and <=0o777.");const c=e.resolve(this.#t,o);K.test(e.relative(this.#t,c))||r.Error.Common("Destination is in the workspace root.");const l=[],d=await t.promises.open(c,"w",a),E=new BigUint64Array([0n]);s.setMaxListeners(Number.MAX_SAFE_INTEGER,d),await d.write(E);let h=0;const u=[];for await(const[s,r]of this.buildIndex(u)){const n=e.join(s.parentPath,s.name),o={pathname:n,dirent:s};if(s.isFile()){const e=t.createReadStream(n),s=d.createWriteStream(z);let a=0;l.push(s),r[B.FILE.OFFSET]=String(h),e.on("data",(t=>a+=t.length));const c=i.Stream.promises.pipeline(e,s);Object.assign(o,{piping:c,readStream:e});const E=async t=>await t(o),u=this[q].map(E),p=await Promise.all([...u,c]);p.pop(),r[B.FILE.EXTEND]=p,r[B.FILE.SIZE]=String(a),h+=a}if(s.isDirectory()){const t=async t=>await t(o),e=this[Z].map(t),i=await Promise.all(e);r[B.DIRECTORY.EXTEND]=i}}E[0]=BigInt(h),await d.write(JSON.stringify(u)),await d.write(E,{position:0});for(const t of l)t.destroy();globalThis.f=d,await d.close()}},exports.FileSystem=class extends H{};