@mateosuarezdev/brpc 1.0.107 → 1.0.108
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 +36 -12
- package/dist/storage/index.js +3 -3
- package/dist/storage/s3/delete.d.ts +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1025,11 +1025,13 @@ All storage utilities are exported from the main package:
|
|
|
1025
1025
|
import {
|
|
1026
1026
|
uploadOne,
|
|
1027
1027
|
uploadMany,
|
|
1028
|
-
uploadSingleObject,
|
|
1029
|
-
generateAndUploadImage,
|
|
1030
|
-
uploadGeneratedVariants,
|
|
1031
|
-
deleteOne,
|
|
1032
|
-
|
|
1028
|
+
uploadSingleObject,
|
|
1029
|
+
generateAndUploadImage,
|
|
1030
|
+
uploadGeneratedVariants,
|
|
1031
|
+
deleteOne,
|
|
1032
|
+
deleteManyKeys,
|
|
1033
|
+
deleteManyObjects,
|
|
1034
|
+
extractObjectVariantKeys,
|
|
1033
1035
|
generateVariants,
|
|
1034
1036
|
getOptimizedImage,
|
|
1035
1037
|
getImageMetadata,
|
|
@@ -1129,13 +1131,35 @@ const { deleted, error } = await deleteOne("images/avatars/abc.webp");
|
|
|
1129
1131
|
if (!deleted) console.error("Failed to delete:", error);
|
|
1130
1132
|
```
|
|
1131
1133
|
|
|
1132
|
-
#### `
|
|
1133
|
-
|
|
1134
|
-
Deletes multiple keys in parallel. Returns a per-key result array.
|
|
1135
|
-
|
|
1136
|
-
```ts
|
|
1137
|
-
const results = await
|
|
1138
|
-
```
|
|
1134
|
+
#### `deleteManyKeys(keys, opts?)`
|
|
1135
|
+
|
|
1136
|
+
Deletes multiple keys in parallel. Returns a per-key result array.
|
|
1137
|
+
|
|
1138
|
+
```ts
|
|
1139
|
+
const results = await deleteManyKeys(variantKeys);
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
#### `extractObjectVariantKeys(objects)` and `deleteManyObjects(objects, opts?)`
|
|
1143
|
+
|
|
1144
|
+
Stored objects hold sparse variants: an image may have `xs` through `xl`, while
|
|
1145
|
+
a document normally has only `default`. `extractObjectVariantKeys` reads every
|
|
1146
|
+
real variant key and removes duplicates. `deleteManyObjects` is the convenience
|
|
1147
|
+
form that extracts those keys and deletes them.
|
|
1148
|
+
|
|
1149
|
+
```ts
|
|
1150
|
+
const results = await deleteManyObjects([voucher, previousAvatar]);
|
|
1151
|
+
|
|
1152
|
+
if (results.some((result) => !result.deleted)) {
|
|
1153
|
+
throw new Error("Could not delete every stored representation");
|
|
1154
|
+
}
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
These functions delete S3 data only; an application owns its database records
|
|
1158
|
+
and domain authorization. When removing a domain record and its objects, run
|
|
1159
|
+
the object delete plus database deletes in its database transaction and abort
|
|
1160
|
+
the transaction on any failed result. S3 is not transactional, so a partial S3
|
|
1161
|
+
failure cannot restore already-deleted siblings; log and reconcile that rare
|
|
1162
|
+
case rather than claiming distributed atomicity.
|
|
1139
1163
|
|
|
1140
1164
|
### Images
|
|
1141
1165
|
|
package/dist/storage/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var
|
|
2
|
+
var T=($)=>$.startsWith("image/"),w=($)=>$.startsWith("video/"),z=($)=>$.startsWith("audio/"),k=($)=>$.startsWith("text/")||$.includes("pdf")||$.includes("word")||$.includes("sheet")||$.includes("presentation")||$.includes("opendocument")||$==="application/rtf",I=($)=>$.includes("zip")||$.includes("rar")||$.includes("tar")||$.includes("7z")||$.includes("gzip"),R=($)=>$==="application/javascript"||$==="application/json"||$==="text/html"||$==="text/css"||$==="application/xml"||$.includes("javascript"),b=($)=>$.startsWith("font/")||$.includes("font"),P=($)=>{if(!$)return"other";let W=$.toLowerCase();if(T(W))return"image";if(w(W))return"video";if(z(W))return"audio";if(k(W))return"document";if(I(W))return"archive";if(R(W))return"code";if(b(W))return"font";return"other"},U={isImage:T,isVideo:w,isAudio:z,isDocument:k,isArchive:I,isCode:R,isFont:b};function $0(){let W=Buffer.from("Hello, this is a test file for S3 upload!","utf-8");return new File([W],"test.txt",{type:"text/plain"})}function q($,W){if(!$||$<1||!W||W<1)return"1:1";let H=(Q,Y)=>Y===0?Q:H(Y,Q%Y),J=H($,W);return`${$/J}:${W/J}`}function H0($,W){if(!$||!W||$<1||W<1)return"1:1";let H=$/W,J=[{ratio:1,label:"1:1"},{ratio:0.8,label:"4:5"},{ratio:0.75,label:"3:4"},{ratio:0.6666666666666666,label:"2:3"},{ratio:0.5625,label:"9:16"},{ratio:1.5,label:"3:2"},{ratio:1.3333333333333333,label:"4:3"},{ratio:1.7777777777777777,label:"16:9"},{ratio:2.3333333333333335,label:"21:9"}],Q=0.03;for(let{ratio:Z,label:_}of J)if(Math.abs(H-Z)/Z<Q)return _;let Y=(Z,_)=>_===0?Z:Y(_,Z%_),L=Y($,W);return`${$/L}:${W/L}`}class X extends Error{code;clientCode;httpStatus;data;cause;static STATUS_MAP={BAD_REQUEST:400,UNAUTHORIZED:401,FORBIDDEN:403,NOT_FOUND:404,METHOD_NOT_SUPPORTED:405,TIMEOUT:408,CONFLICT:409,PRECONDITION_FAILED:412,PAYLOAD_TOO_LARGE:413,UNPROCESSABLE_CONTENT:422,TOO_MANY_REQUESTS:429,CLIENT_CLOSED_REQUEST:499,INTERNAL_SERVER_ERROR:500,NOT_IMPLEMENTED:501,BAD_GATEWAY:502,SERVICE_UNAVAILABLE:503,GATEWAY_TIMEOUT:504};constructor($){super($.message);if(this.name="BRPCError",this.code=$.code,this.clientCode=$.clientCode,this.httpStatus=X.STATUS_MAP[$.code],this.data=$.data,this.cause=$.cause,Error.captureStackTrace)Error.captureStackTrace(this,X)}toJSON(){return{name:this.name,code:this.code,clientCode:this.clientCode,message:this.message,data:this.data,httpStatus:this.httpStatus}}static badRequest($,W,H){return new X({code:"BAD_REQUEST",message:$,clientCode:W,data:H})}static unauthorized($="Unauthorized",W,H){return new X({code:"UNAUTHORIZED",message:$,clientCode:W,data:H})}static forbidden($="Forbidden",W,H){return new X({code:"FORBIDDEN",message:$,clientCode:W,data:H})}static notFound($="Not Found",W,H){return new X({code:"NOT_FOUND",message:$,clientCode:W,data:H})}static preconditionFailed($="Precondition failed",W,H){//! Intentionally maps to NOT_FOUND instead of PRECONDITION_FAILED — this was changed to fix a bug
|
|
3
3
|
//! but the exact cause is unknown. Suspected: some client/proxy/middleware misbehaved on 412
|
|
4
4
|
//! (not a browser auto-retry issue — 412 doesn't trigger those). Investigate before changing back.
|
|
5
|
-
return new
|
|
5
|
+
return new X({code:"NOT_FOUND",message:$,clientCode:W,data:H})}static conflict($,W,H){return new X({code:"CONFLICT",message:$,clientCode:W,data:H})}static unprocessableContent($,W,H){return new X({code:"UNPROCESSABLE_CONTENT",message:$,clientCode:W,data:H})}static tooManyRequests($="Too many requests",W,H){return new X({code:"TOO_MANY_REQUESTS",message:$,clientCode:W,data:H})}static internalServerError($="Internal Server Error",W,H){return new X({code:"INTERNAL_SERVER_ERROR",message:$,clientCode:W,data:H})}static timeout($="Request timeout",W,H){return new X({code:"TIMEOUT",message:$,clientCode:W,data:H})}}import m from"sharp";var E={xs:200,sm:400,md:800,lg:1200,xl:1920},A=1920;async function S($,W,H,J,Q){try{let{data:Y,info:L}=await m($,{limitInputPixels:1e8}).rotate().resize({width:W,height:W,fit:"inside",withoutEnlargement:!0}).webp({quality:H}).toBuffer({resolveWithObject:!0});return{buffer:Y,object:{name:J.replace(/\.[^/.]+$/,".webp"),resolvedType:"image",metadata:{type:"image/webp",size:Y.length,extension:".webp",width:L.width,height:L.height,aspectRatio:L.width&&L.height?L.width/L.height:1,aspectRatioStr:L.width&&L.height?q(L.width,L.height):"1:1",acl:Q}}}}catch(Y){throw X.conflict(`Failed to optimize image: ${Y instanceof Error?Y.message:"Unknown error"}`)}}async function B($){if(!$)throw X.conflict("A file must be provided");let W,H="unknown.webp";if(typeof $==="string"){if(console.log("I'm going to fail here for file",$),!$.startsWith("data:"))throw console.log("I never got here"),X.conflict("Invalid base64 format");let[,J]=$.split(",");if(!J)throw X.conflict("Invalid base64 data");W=Buffer.from(J,"base64")}else{if(!U.isImage($.type))throw X.conflict("File must be an image");let J=await $.arrayBuffer();W=Buffer.from(J),H=$.name??H}return{buffer:W,name:H}}async function x0($,{size:W,quality:H,acl:J}={}){let Q=await B($);return S(Q.buffer,W??A,H??80,Q.name,J??"public-read")}async function D0($,{name:W="unknown.webp",size:H,quality:J,acl:Q}={}){if(!$?.length)throw X.conflict("A buffer must be provided");return S($,H??A,J??80,W,Q??"public-read")}async function v($,{quality:W=80,acl:H="public-read"}={}){let J=await B($),Q=await Promise.all(Object.entries(E).map(async([Y,L])=>{let Z=await S(J.buffer,L,W,J.name,H);return[Y,Z]}));return{variants:Object.fromEntries(Q)}}import n from"sharp";import{randomUUID as y}from"crypto";import{extname as u}from"path";async function C($,W,H){if(!W)return{data:null,error:Error("File not found")};let J=$.replace(/^\/+|\/+$/g,""),Q=u(W.name),Y=`${y()}${Q}`,L=`${J}/${Y}`,Z=`./buckets/${J}/${Y}`;try{let _=await Bun.write(Z,W);return{data:{key:L,file:W,bytesWritten:_},error:null}}catch(_){return{data:null,error:Error(`Failed to upload ${W.name} to local filesystem`)}}}async function f($,W,H){if(!W||W.length===0)return[];let J=W.map((Y)=>{let L=H?.(Y)??Y.name;return C($,Y,L)});return await Promise.all(J)}async function d($){let W=Bun.file(`./buckets/${$}`);if(!await W.exists())return null;return W}async function p($){let W=Bun.file(`./buckets/${$}`);if(!await W.exists())return!1;return await W.delete(),!0}var V={uploadOne:C,uploadMany:f,getOne:d,deleteOne:p};async function S0($){let W=`./cache/images/${$.key}_${$.width}_${$.quality}.webp`,H=await V.getOne(W);if(H){if(!U.isImage(H.type))return null;return H}let J=$.key,Q=await V.getOne(J);if(!Q)return null;if(!U.isImage(Q.type))return null;let Y=await Q.arrayBuffer(),L=await n(Y).resize($.width,null,{withoutEnlargement:!0}).webp({quality:$.quality??75}).toBuffer();await Bun.write(W,L);let Z=Bun.file(W);if(!await Z.exists())return null;return Z}import s from"sharp";async function T0($){if(!$?.length)return null;try{let W=await s($).metadata(),H=W.width??0,J=W.height??0,Q=W.height>0?H/J:1;return{type:`image/${W.format}`,size:W.size??0,extension:`.${W.format}`,width:H,height:J,aspectRatio:Q,aspectRatioStr:q(H,J)}}catch(W){return console.warn("Failed to read image metadata:",W),null}}var g=process.env.AWS_FOLDER,h=g?`${g}/`:"";var{s3:i}=globalThis.Bun;import{extname as t}from"path";function c($={}){return $}var{s3:l}=globalThis.Bun;function o($){return[...new Set($.flatMap(({variants:W})=>Object.values(W).flatMap((H)=>H&&typeof H==="object"&&("key"in H)&&typeof H.key==="string"&&H.key.length>0?[H.key]:[])))]}async function r($,W){if(!$||typeof $!=="string")throw X.badRequest("Key is required");let H=$.trim();if(!H||H.length===0)throw X.badRequest("Key must be a non-empty string");try{return await l.delete(H),{key:H,deleted:!0,error:null}}catch(J){return{key:$,deleted:!1,error:J}}}async function F($,W){return(await Promise.allSettled($.map((J)=>r(J,W)))).map((J,Q)=>{if(J.status==="fulfilled")return J.value;return{key:$[Q],deleted:!1,error:J.reason}})}async function b0($,W){return F(o($),W)}async function N($,W,H){let J="buffer"in W?W:{buffer:W,object:{name:W.name,resolvedType:P(W.type),metadata:{type:W.type,extension:t(W.name),size:W.size,acl:H?.acl??"public-read"}}};if("buffer"in W){if(!W.object?.name||!W.object?.metadata)throw X.badRequest("Complete metadata required when uploading buffer")}let Q=$.length>1?`${$.replace(/^\/+|\/+$/g,"")}/`:"",Y=J.object.metadata.extension.startsWith(".")?J.object.metadata.extension:`.${J.object.metadata.extension}`,L=J.object.name?J.object.name:`${crypto.randomUUID()}${Y}`,Z=`${h}${Q}${L}`,_=i.file(Z),G=J?.object?.metadata?.acl??"public-read",M=G!=="public-read"&&G!=="public-read-write",D=await _.write(J.buffer,{acl:G,type:J.object.metadata.type});return{uploadedBy:H?.uploadedBy,key:Z,name:J.object.name,resolvedType:J.object.resolvedType,provider:"s3",isPrivate:M,metadata:{...J.object.metadata,size:D,acl:G},isActive:!0}}async function y0($,W,H={throwIf:"any"}){let J=await Promise.allSettled(W.map((L)=>N($,L,{acl:H?.acl,uploadedBy:H?.uploadedBy}))),Q=[],Y=[];if(J.forEach((L)=>{if(L.status==="fulfilled"){if(Q.push(L.value),H?.externalKeys)H.externalKeys.push(L.value.key)}else Y.push(L.reason)}),H?.throwIf==="any"&&Y.length>0||H?.throwIf==="all"&&Y.length===W.length)throw X.internalServerError(`Upload failed: ${Y.length} of ${W.length}`);return{data:Q,errors:Y.length?Y:void 0}}async function a($,W,H){let J=H?.baseId??crypto.randomUUID(),Q=Object.values(W.variants).find(Boolean),Y=await Promise.allSettled(Object.entries(W.variants).map(async([M,D])=>{if(!D)return[M,null];let K=D.object.metadata.extension,j=`${J}_${M}${K}`,O=await N($,{...D,object:{...D.object,name:j}},H);if(!O.metadata)throw X.internalServerError("Upload returned no metadata, cannot build a storage variant");let x=O.metadata;return[M,{key:O.key,name:j,metadata:{type:x.type,extension:x.extension,size:x.size,hash:x.hash,width:x.width,height:x.height,aspectRatio:x.aspectRatio,aspectRatioStr:x.aspectRatioStr,resolution:x.resolution,bitrate:x.bitrate,codec:x.codec,fps:x.fps}}]})),L=Y.flatMap((M)=>M.status==="fulfilled"?[M.value]:[]),Z=Y.flatMap((M)=>M.status==="rejected"?[M.reason]:[]);if(Z.length>0){let M=L.flatMap(([,j])=>j?.key?[j.key]:[]),K=(await F(M)).filter(({deleted:j})=>!j);if(K.length>0)console.error("Failed to clean up partially uploaded image variants",{keys:M,failures:K});throw Z[0]}let _=(H?.acl??"public-read")!=="public-read"&&(H?.acl??"public-read")!=="public-read-write",G={};for(let[M,D]of L)if(D)G[M]=D;return{shared:{name:Q.object.name,resolvedType:Q.object.resolvedType,provider:"s3",acl:H?.acl??"public-read",isPrivate:_,uploadedBy:H?.uploadedBy},variants:G}}async function u0($,W,H){let J=await N($,W,H);if(!J.metadata)throw X.internalServerError("Upload returned no metadata, cannot build a storage object");let Q=J.name??J.key.split("/").pop()??J.key;return{shared:{name:Q,resolvedType:J.resolvedType,provider:J.provider,acl:J.metadata.acl,isPrivate:J.isPrivate,uploadedBy:H?.uploadedBy},variants:c({default:{key:J.key,name:Q,metadata:{type:J.metadata.type,extension:J.metadata.extension,size:J.metadata.size,hash:J.metadata.hash,width:J.metadata.width,height:J.metadata.height,aspectRatio:J.metadata.aspectRatio,aspectRatioStr:J.metadata.aspectRatioStr,resolution:J.metadata.resolution,bitrate:J.metadata.bitrate,codec:J.metadata.codec,fps:J.metadata.fps}}})}}async function f0($,W,H){if(Array.isArray(W))throw Error("File can't be an array, pass a single file");let J=await v(W,{quality:H?.quality,acl:H?.acl});return a($,J,{acl:H?.acl,uploadedBy:H?.uploadedBy})}var{s3:e}=globalThis.Bun;async function n0($,W={debug:!0}){try{return{exists:await e.exists($),error:null}}catch(H){if(W?.debug)console.error("There was an error checking for file existance",H);return{exists:null,error:H}}}export{u0 as uploadSingleObject,N as uploadOne,y0 as uploadMany,a as uploadGeneratedVariants,D0 as optimizeImageBuffer,x0 as optimizeImage,P as mimeTypeToResolvedType,S0 as getOptimizedImage,T0 as getImageMetadata,H0 as getAspectRatioStringRound,q as getAspectRatioString,v as generateVariants,$0 as generateTestFile,f0 as generateAndUploadImage,U as fileTypePredicates,o as extractObjectVariantKeys,r as deleteOne,b0 as deleteManyObjects,F as deleteManyKeys,c as createUploadVariants,n0 as checkFileExistance};
|
|
6
6
|
|
|
7
|
-
//# debugId=
|
|
7
|
+
//# debugId=BF9212D9C6E0325364756E2164756E21
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { DeleteOneResult } from "./types";
|
|
2
|
+
type ObjectWithVariants = {
|
|
3
|
+
variants: object;
|
|
4
|
+
};
|
|
5
|
+
/** Returns the unique S3 keys represented by one or more sparse stored objects. */
|
|
6
|
+
export declare function extractObjectVariantKeys<T extends ObjectWithVariants>(objects: T[]): string[];
|
|
2
7
|
export declare function deleteOne(key: string, opts?: {
|
|
3
8
|
debug?: boolean;
|
|
4
9
|
}): Promise<DeleteOneResult>;
|
|
@@ -6,6 +11,11 @@ export declare function deleteOne(key: string, opts?: {
|
|
|
6
11
|
* Deletes multiple S3 keys in parallel.
|
|
7
12
|
* Returns per-key result so caller can handle failures/rollback.
|
|
8
13
|
*/
|
|
9
|
-
export declare function
|
|
14
|
+
export declare function deleteManyKeys(keys: string[], opts?: {
|
|
10
15
|
debug?: boolean;
|
|
11
16
|
}): Promise<DeleteOneResult[]>;
|
|
17
|
+
/** Deletes every stored representation of the supplied sparse objects. */
|
|
18
|
+
export declare function deleteManyObjects<T extends ObjectWithVariants>(objects: T[], opts?: {
|
|
19
|
+
debug?: boolean;
|
|
20
|
+
}): Promise<DeleteOneResult[]>;
|
|
21
|
+
export {};
|