1 line
4.5 KiB
JavaScript
1 line
4.5 KiB
JavaScript
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)}`}} |