Initial
This commit is contained in:
1
resources/app/dist/files/downloader.mjs
vendored
Normal file
1
resources/app/dist/files/downloader.mjs
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import EventEmitter from"node:events";import fs from"node:fs";import path from"node:path";import{fetchWithTimeout}from"../../common/utils/http.mjs";export default class FileDownloader extends EventEmitter{constructor(e,t){super(),this._url=e,this._localPath=t}_url;_localPath;download=()=>new Promise((async e=>{const t=path.dirname(this._localPath);if(!fs.existsSync(t))return this.emit("error",new Error(`Local directory ${t} does not exist!`)),void e();try{fs.accessSync(t,fs.constants.W_OK)}catch(r){return this.emit("error",new Error(`You do not have permission to write content in ${t}: ${r.message}`)),void e()}let r;try{r=await this.#e()}catch(t){return t.message=`An unexpected error occurred when downloading file ${this._url}: ${t.message}`,this.emit("error",t),void e()}this.emit("fetched");const o=fs.createWriteStream(this._localPath);o.on("error",(e=>this.emit("error",e))),o.on("finish",(()=>{o.close(),e()}));const i=Number(r.headers.get("content-length")||1e8);let s=0;for await(const e of r.body)s+=e.length,o.write(e),this.emit("progress",s,i);o.end()}));async#e(){let e;try{e=await fetchWithTimeout(this._url,{referrerPolicy:"no-referrer"})}catch(t){if(406!==t.code)throw t;e=await fetchWithTimeout(this._url,{referrerPolicy:"no-referrer",mode:"same-origin",headers:{"sec-fetch-mode":"same-origin"}})}return e}}
|
||||
1
resources/app/dist/files/files.mjs
vendored
Normal file
1
resources/app/dist/files/files.mjs
vendored
Normal file
File diff suppressed because one or more lines are too long
1
resources/app/dist/files/local.mjs
vendored
Normal file
1
resources/app/dist/files/local.mjs
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import fs from"node:fs";import path from"node:path";import{minimatch}from"minimatch";import AbstractFileStorage from"./storage.mjs";import Files from"./files.mjs";import{encodeURL}from"../../common/utils/helpers.mjs";import{MEDIA_MIME_TYPES}from"../../common/constants.mjs";import mime from"mime-types";export default class LocalFileStorage extends AbstractFileStorage{constructor(t,e){super(t),this.root=e}async createDirectory(t,e={}){if(this.root!==paths.data||!e.isAdmin)throw new Error("You may not create directories in this location");const r=path.join(this.root,t),o=path.relative(this.root,r);if(o&&(o.startsWith("..")||path.isAbsolute(o)))throw new Error("You are not permitted to create a subdirectory in this location");return fs.mkdirSync(decodeURIComponent(r)),r}getDirectories(t){return fs.readdirSync(t).reduce(((e,r)=>{const o=path.join(t,r);return this.isDirectory(o)&&e.push(o),e}),[])}async getFiles({target:t=".",extensions:e=[],wildcard:r=!1,isAdmin:o=!1,type:i}={}){const s=t;t=decodeURIComponent(t);let n=path.join(this.root,t);const a=e.map((t=>t.toLowerCase())),c=path.relative(this.root,n);c&&(c.startsWith("..")||path.isAbsolute(c))&&(n=this.root,t="");let h="";(r||fs.existsSync(n)&&!this.isDirectory(n))&&(h=path.basename(t),t=path.dirname(t),n=path.dirname(n));const l=Object.keys(this.configuration);l.sort(((t,e)=>{const r=e.split("/").length-t.split("/").length;return 0!==r?r:""===t?1:""===e?-1:t.compare(e)}));let p=l.find((t=>RegExp(`^${t}/`).test(`${s}/`)))||"";const m=this.configuration[p]||{private:!1,gridSize:null};if(!fs.existsSync(n)||m.private&&!o)throw new Error(`Directory ${t} does not exist or is not accessible in this storage location`);const d={target:encodeURL(t),private:m.private,gridSize:m.gridSize,dirs:[],privateDirs:[],files:[],extensions:e},u=fs.readdirSync(n).map((t=>path.join(n,t))).reduce(((e,s)=>{const n=path.basename(s);if(n.startsWith("."))return e;if(r&&!minimatch(n,h))return e;if(this.isDirectory(s)){const r=encodeURL(path.posix.join(t,n));if(r in this.configuration&&this.configuration[r].private){if(!o)return e;e.privateDirs.push(r)}return e.dirs.push(s),e}if("folder"===i)return e;const c=path.extname(n).toLowerCase();return(a.length&&a.includes(c)||!a.length&&""!==c)&&e.files.push(s),e}),d);return u.dirs=this.toClientPaths(u.dirs,this.root),u.files=this.toClientPaths(u.files,this.root),u}isDirectory(t){let e=null;try{e=fs.lstatSync(t)}catch(t){return!1}if(e.isDirectory())return!0;if(e.isSymbolicLink())try{return fs.statSync(t).isDirectory()}catch(t){return!1}return!1}async upload(t,{target:e,contentType:r,overwrite:o=!1}={}){if(this.root!==paths.data)throw new Error("You may not upload to this location");const i=path.join(this.root,e);if(!fs.existsSync(i))throw new Error(`Target directory ${i} does not exist.`);const s=path.relative(this.root,i);if(s&&(s.startsWith("..")||path.isAbsolute(s)))throw new Error("You may not upload files to this location.");const n=path.join(i,t.name),a=this.toClientPath(n,this.root);if(fs.existsSync(n)){if(!o)throw new Error("You may not upload non-media files which would overwrite an existing file on the server.");const t=r??mime.lookup(n);if(!MEDIA_MIME_TYPES.includes(t))throw new Error(`You may not overwrite an existing file with a MIME type of ${t}.`)}return new Promise((r=>{t.mv(n,(o=>{const i=""!==e?e:"User Data storage";if(o)throw o.message=`Unable to place ${t.name} in ${i}`,o;const s=`${t.name} saved to ${i}`;logger.info(s),r({status:"success",message:s,path:a})}))}))}toClientPath(t,e){return LocalFileStorage.toClientPath(t,e)}static toClientPath(t,e){const r=Files.standardizePath(path.relative(e,t));try{return decodeURIComponent(r)!==r?r:encodeURL(r)}catch(t){return encodeURL(r)}}}
|
||||
1
resources/app/dist/files/s3.mjs
vendored
Normal file
1
resources/app/dist/files/s3.mjs
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import fs from"node:fs";import path from"node:path";import{S3 as S3Client}from"@aws-sdk/client-s3";import{minimatch}from"minimatch";import AbstractFileStorage from"./storage.mjs";import{encodeURL,mergeObject}from"../../common/utils/helpers.mjs";export default class S3FileStorage extends AbstractFileStorage{constructor(t,e,i){super(t),this.config=e,this.client=new S3Client(e),this.buckets=i}static fromConfig(t,e){if(!e)return null;const i={};let n=null;if(!0===e)logger.info("Configuring AWS credentials using system configuration or environment variables");else if("string"==typeof e){let t=e;if(fs.existsSync(t)||(t=path.join(paths.config,e)),!fs.existsSync(t)){const e=new Error(`The requested AWS config path ${t} does not exist!`);return logger.error(e),null}const s=JSON.parse(fs.readFileSync(t,"utf-8"));mergeObject(i,s),s.buckets instanceof Array&&(n=s.buckets),logger.info(`Configured AWS credentials using ${t}`)}try{return new this(t,i,n)}catch(t){return logger.error(t),null}}async createDirectory(t,{bucket:e}={}){const{logger:i}=global;let n=t;n=path.posix.join(n.slugify({lowercase:!1}),"/");let s=!1;try{s=await this.client.headObject({Bucket:e,Key:n})}catch(t){}if(s)throw new Error(`The S3 key ${n} already exists and cannot be created`);return await this.client.putObject({Bucket:e,Key:n,Body:""}),i.info(`Created subdirectory in S3 s3://${e}/${t}`),t}async getFiles({target:t=".",extensions:e=[],wildcard:i=!1,bucket:n="",isAdmin:s=!1,type:o}={}){const r={target:t=decodeURIComponent(t),private:!1,gridSize:null,dirs:[],privateDirs:[],files:[],extensions:e.map((t=>t.toLowerCase()))};if(!n)return r.dirs=await this.#t(),r;let a="";i&&(a=path.basename(t),t=path.dirname(t)),t&&!t.endsWith("/")&&(t+="/");const c=this.configuration[n]||{},l=Object.keys(c);l.sort(((t,e)=>{const i=e.split("/").length-t.split("/").length;return 0!==i?i:""===t?1:""===e?-1:t.compare(e)}));let u=l.find((e=>RegExp(`^${e}/`).test(t)));const p=u?c[u]:{private:!1,gridSize:null};if(r.private=p.private,p.private&&!s)return r;const{keys:h,prefixes:f}=await this.#e(n,t);for(let t of f){const e=t.endsWith("/")?t.slice(0,-1):t;if(e in c&&c[e].private){if(!s)continue;r.privateDirs.push(e)}r.dirs.push(e)}if("folder"!==o)for(let e of h){let n=path.posix.relative(t,e);""!==n&&(i&&!minimatch(n,a)||r.extensions.length&&!r.extensions.includes(path.extname(e).toLowerCase())||r.files.push(e))}return r.bucket=n,r.files=this.toClientPaths(r.files,n),r}async#t(){if(this.buckets)return this.buckets;const{logger:t}=global;t.info("Searching for allowed buckets for S3 storage.");const e=[],{Buckets:i}=await this.client.listBuckets({});for(const t of i??[]){await this.#i(t.Name)&&e.push(t.Name)}return this.buckets=e}async#i(t){try{return await this.client.headBucket({Bucket:t}),!0}catch(t){return!1}}async#e(t,e){const i={Bucket:t,MaxKeys:1e3,Delimiter:"/"};"."!==e&&(i.Prefix=e);let n=!0;const s=[],o=new Set,r=async t=>{const e=await this.client.listObjectsV2(t);return{keys:(e.Contents||[]).map((t=>t.Key)),prefixes:(e.CommonPrefixes||[]).map((t=>t.Prefix)),nextToken:e.IsTruncated?e.NextContinuationToken:null}};for(;n;){const t=await r(i);s.push(...t.keys);for(let e of t.prefixes)o.add(e);t.nextToken&&(i.ContinuationToken=t.nextToken),null===t.nextToken&&(n=!1)}return{keys:s,prefixes:o}}async upload(t,{bucket:e,contentType:i,target:n}={}){const s=path.posix.join(n,t.name);let o;try{o=await this.client.putObject({Bucket:e,Key:s,Body:t.data,ContentType:i||"text/plain",ACL:"public-read"})}catch(t){throw t.message=`File upload failed: ${t.message}`,t}const r=this.toClientPath(s,e),a=`Uploaded file ${o.name} to ${r}`;return logger.info(a),{status:"success",message:a,path:r}}#n;get endpoint(){return this.#n}async identifyEndpoint(){const[t]=await this.#t();if(!t)return;const e="extractEndpointMiddleware";this.client.middlewareStack.add((e=>i=>(this.#s(i.request,t),e(i))),{step:"build",name:e});try{await this.client.headBucket({Bucket:t})}finally{this.client.middlewareStack.remove(e)}}#s(t,e){let{hostname:i,port:n,protocol:s,path:o}=t;this.config.forcePathStyle?o=o.replace(`/${e}/`,"/"):i=i.replace(new RegExp(`^${e}\\.`),"");let r=i;Number.isFinite(n)&&(r+=`:${n}`);const[a]=o.split("?"),c=`${s}//${r}${o}`;this.#n={host:r,hostname:i,port:n,protocol:s,path:o,pathname:a,href:c}}toClientPath(t,e){if(!this.#n)throw new Error("Unable to determine S3 client path with no available endpoint.");const{host:i,protocol:n,pathname:s}=this.endpoint;return this.config.forcePathStyle?`${n}//${i}/${e}${s}${encodeURL(t)}`:`${n}//${e}.${i}${s}${encodeURL(t)}`}}
|
||||
1
resources/app/dist/files/storage.mjs
vendored
Normal file
1
resources/app/dist/files/storage.mjs
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import path from"node:path";export default class AbstractFileStorage{constructor(t,...e){this.storageName=t}get configuration(){const t=config.files.configuration;return this.storageName in t||(t[this.storageName]={}),t[this.storageName]}async createDirectory(t,e={}){throw new Error("Not Implemented")}async getFiles({target:t,extensions:e,wildcard:r,bucket:o,type:a}={}){throw new Error("Not Implemented")}async upload(t,e={}){throw new Error("Not Implemented")}toClientPaths(t,e){return t.map((t=>this.toClientPath(t,e)))}toClientPath(t,e){throw new Error("Not Implemented")}}
|
||||
Reference in New Issue
Block a user