UNPKG

bunny-transfer

Version:

Transfer files to and from your storage zones with bunny.net in a single cli command 🐰

3 lines (2 loc) 19.1 kB
import { createRequire } from 'module';const require = createRequire(import.meta.url); import{argument as V}from"pastel";import rr from"react";import{z as D}from"zod";import at from"camelcase-keys";import it from"fast-deep-equal/es6/index.js";import z from"fs-extra";import st from"is-obj";import X from"p-map";import{parse as pt}from"smol-toml";import Qe from"node:os";var m=Qe.cpus().length;function _(e,t){return e.filter((r,o)=>o===e.findIndex(n=>n[t]===r[t]))}import{createBunnyApiClient as et}from"bunny-sdk";import tt from"is-obj";function H({firstName:e,lastName:t}){if(e&&t)return`${e} ${t}`;if(t)return t;if(e)return e}async function U({accessKey:e,profile:t=e}){let o=await et({accessKey:e}).user.get().catch(a=>{throw tt(a)&&"responseStatusCode"in a&&a.responseStatusCode===401?new Error(`Cannot find user with accessKey "${e}"`):a});if(!o)throw new Error(`Cannot find user with accessKey "${e}"`);let n=H({firstName:o.firstName,lastName:o.lastName});return{accessKey:e,email:o.email,id:o.id,...n&&{name:n},profile:t}}import rt from"node:path";import ot from"untildify";import{z as Z}from"zod";var O="~/.bunny/credentials",R=rt.resolve(ot(O)),Y=Z.object({accessKey:Z.string(),email:Z.string(),id:Z.string(),name:Z.string().optional(),profile:Z.string()});import{stringify as nt}from"smol-toml";function q(e,t){return e.reduce((r,o)=>{let{[t]:n,...a}=o;return(typeof n=="string"||typeof n=="number"||typeof n=="symbol")&&(r[n]=a),r},{})}function J(e){return nt(q(e.map(t=>({...t,profile:t.profile??t.accessKey})),"profile"))}async function Q({accessKey:e=[],credentialsPath:t=R,profile:r=[]}={}){let o=typeof e=="string"?[e]:e,n=typeof r=="string"?[r]:r,a=await z.pathExists(t),i=[];if(a){let c=await z.readFile(t,"utf8"),h=pt(c),d=!1;if(i=await X(Object.entries(h),async([g,S])=>{let u=Y.parse({...st(S)?at({...S,profile:g}):{}}),F=await U({accessKey:u.accessKey,profile:g});return it(u,F)||(d=!0),F},{concurrency:m}),d){let g=J(i);await z.outputFile(t,g)}}let s=await X(o,async c=>U({accessKey:c,profile:c}),{concurrency:m}),l=_([...i,...s],"profile");return n.length>0?l.filter(c=>n.includes(c.profile??"")):l}async function ee(e){let t=await Q(e);return[...new Set(t.map(o=>o.accessKey))]}import{option as ct}from"pastel";import{z as re}from"zod";var te=re.string().trim(),mt=re.preprocess(e=>process.env.BUNNY_ACCESS_KEY&&!e?process.env.BUNNY_ACCESS_KEY.split(","):e,te.or(te.array()).optional().transform(e=>typeof e=="string"?[e]:e)),oe=mt.describe(ct({alias:"k",description:"Bunny API Access Key",valueDescription:"uuid"}));import{option as dt}from"pastel";import{z as ae}from"zod";var ne=ae.string().trim().min(1),lt=ae.preprocess(e=>process.env.BUNNY_PROFILE&&!e?process.env.BUNNY_PROFILE.split(","):e,ne.or(ne.array()).optional().transform(e=>typeof e=="string"?[e]:e)),ie=lt.describe(dt({alias:"p",description:"Bunny user profile name",valueDescription:"string"}));import ft from"node:path";import{option as yt}from"pastel";import gt from"untildify";import{z as se}from"zod";var ut=se.preprocess(e=>{if(process.env.BUNNY_SHARED_CREDENTIALS_FILE&&!e){let t=process.env.BUNNY_SHARED_CREDENTIALS_FILE.trim();return ft.resolve(gt(t))}return e},se.string().trim().optional().default(R)),pe=ut.describe(yt({alias:"c",defaultValueDescription:O,description:"Bunny shared credentials file path",valueDescription:"file-path"}));import{createBunnyApiClient as Gt,createEdgeStorageApiClient as G}from"bunny-sdk";import Wt from"chalk";import P from"tiny-invariant";var ce=new Intl.ListFormat("en",{style:"long",type:"conjunction"});import bt from"tildify";var me=process.cwd();function b(e){return e.startsWith(me)?"."+e.slice(me.length):bt(e)}function p(){}import le from"fast-glob";import kt from"fs-extra";import wt from"p-map";import{createHash as ht}from"node:crypto";import{pipeline as St}from"node:stream/promises";async function de(e){let t=ht("sha256");return await St(e,t),t.digest("hex").toUpperCase()}async function j({absoluteDir:e,dispatchTaskEnd:t=p,dispatchTaskStart:r=p}){e=e.at(-1)==="/"?e:e+"/";let o=e.slice(0,-1).length,n=le.convertPathToPattern(e)+"**/*",a=await le(n,{absolute:!0,concurrency:m,dot:!0,onlyFiles:!0}),i=await wt(a,async l=>{r(b(l));let c=await de(kt.createReadStream(l));return t(b(l)),c},{concurrency:m}),s=new Map;for(let l=0;l<a.length;l++)s.set(a[l].slice(o),i[l]);return s}import xt from"p-map";import Ft from"camelcase-keys";function w(e){if(!e.ok)throw new Error(`${e.status} ${e.statusText}`,{cause:e});return e}function A({file:e,folder:t,storageZone:r}){let o=e??t;return o?o.isDirectory?"https://"+r.storageHostname+o.path+o.objectName+"/":"https://"+r.storageHostname+o.path+o.objectName:"https://"+r.storageHostname+"/"+r.name+"/"}async function C({folder:e,storageZone:t}){let r=A({folder:e,storageZone:t});return await fetch(r,{headers:{accesskey:t.password}}).then(w).then(async n=>n.json()).then(n=>Ft(n))}async function v({dispatchTaskEnd:e=p,dispatchTaskStart:t=p,storageZone:r}){t("./");let o=await C({storageZone:r});e("./");let n=`/${r.name}`;return await fe({dispatchTaskEnd:e,dispatchTaskStart:t,fileMap:new Map,filePathPrefix:n,files:o??[],storageZone:r})}async function fe({dispatchTaskEnd:e=p,dispatchTaskStart:t=p,fileMap:r,filePathPrefix:o,files:n,storageZone:a}){if(n.length===0)return r;let i=[];for(let c of n)c.isDirectory===!1&&r.set((c.path+c.objectName).slice(o.length),c.checksum),c.isDirectory===!0&&i.push(c);let l=(await xt(i,async c=>{let h=(c.path+c.objectName).slice(o.length);t("."+h);let d=await C({folder:c,storageZone:a});return e("."+h),d},{concurrency:m})).flat(1);return l.length>0&&await fe({fileMap:r,filePathPrefix:o,files:l,storageZone:a}),r}function L(e,t){e=new Map(e),t=new Map(t);for(let[r,o]of e.entries())t.get(r)===o&&(e.delete(r),t.delete(r));return[e,t]}import he from"fs-extra";import At from"p-map";function ye(e){return typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1}import xo from"tiny-invariant";async function ge({bunnyApiClient:e,id:t}){return e.storagezone.byId(t).get()}import Ao from"tiny-invariant";async function ue({bunnyApiClient:e,includeDeleted:t=!1,name:r}){let o=await e.storagezone.get({queryParameters:{includeDeleted:t,page:1,perPage:1,search:r}});return o?.items?.[0]?.name===r?o?.items?.[0]:void 0}import Ct from"node:path";import Zt from"untildify";function be(e){return[...new Set(e.map(t=>{if(t.at(0)==="/"||t.startsWith("./")||t.startsWith("../")||t.startsWith("~/")||t==="."||t==="~"){let r=Ct.resolve(Zt(t));return r.at(-1)!=="/"?r+"/":r}return t}))]}async function Se({bunnyApiClients:e,dispatchTaskEnd:t=p,dispatchTaskStart:r=p,locations:o}){if(o=be(o),o.length<2)throw new Error(`No destination "to" locations found in "${JSON.stringify(o)}" after duplicates were removed.`);return await At(o,async(a,i)=>{r(a.at(0)==="/"?b(a):a);let s=await Et({bunnyApiClients:e,index:i,input:a});return t(a.at(0)==="/"?b(a):a),s},{concurrency:m})}async function Et({bunnyApiClients:e,index:t,input:r}){if(r.at(0)==="/"){if(t===0&&!await he.pathExists(r))throw new Error(`Path "${r}" does not exist`);return t>0&&await he.ensureDir(r),{path:r,type:"local"}}if(r.includes("/"))throw new Error(`Location "${r}" is invalid. Local paths must be a relative path starting with "./" or an absolute path starting with "/". Storage zone names cannot include "/".`);for(let o of e){if(ye(r)){let a=await ge({bunnyApiClient:o,id:Number.parseInt(r,10)});if(a)return{bunnyApiClient:o,storageZone:a,type:"storage-zone"};continue}let n=await ue({bunnyApiClient:o,name:r});if(n)return{bunnyApiClient:o,storageZone:n,type:"storage-zone"}}throw new Error(`Storage zone "${r}" cannot be found. Does it exist in your account?`)}import Rt from"fs-extra";import vt from"node:path";import Lt from"p-map";import ke from"fast-glob";import Mt from"fs-extra";async function we({absoluteDir:e}){e=e.at(-1)==="/"?e:e+"/";let t=ke.convertPathToPattern(e)+"**/*",r=await ke(t,{absolute:!0,concurrency:m,dot:!0,onlyDirectories:!0});r.sort((o,n)=>n.length-o.length);for(let o of r)await Mt.rmdir(o).catch(()=>{})}import $ from"fs-extra";import B from"node:path";import{pipeline as xe}from"node:stream/promises";import Ce from"p-map";import Pt from"node:path";import{Readable as Tt}from"node:stream";import{ReadableStream as Dt}from"node:stream/web";var Fe=new TextEncoder;async function E({filePath:e,storageZone:t}){let r=Pt.parse(e);r.dir.at(0)==="/"&&(r.dir=r.dir.slice(1));let o=await fetch(`https://${t.storageHostname}/${t.name}/${r.dir}/${r.base}`,{headers:{accesskey:t.password}}).then(w);return o.body?o.body:Dt.from(Tt.from(Fe.encode("")))}async function Ze({absoluteDir:e,dispatchTaskEnd:t=p,dispatchTaskStart:r=p,storageZone:o,storageZoneFileMap:n}){let a=new Map(n);await Ce(a.keys(),async i=>{if(!i.endsWith(".html")){r("."+i);let s=B.join(e,i);await $.ensureDir(B.dirname(s)),await xe(await E({filePath:i,storageZone:o}),$.createWriteStream(s)),a.delete(i),t("."+i)}},{concurrency:m}),await Ce(a.keys(),async i=>{r("."+i);let s=B.join(e,i);await $.ensureDir(B.dirname(s)),await xe(await E({filePath:i,storageZone:o}),$.createWriteStream(s)),a.delete(i),t("."+i)},{concurrency:m})}async function Ae({absoluteDir:e,dispatchTaskEnd:t=p,dispatchTaskStart:r=p,localFileMap:o,storageZone:n,storageZoneFileMap:a}){await Ze({absoluteDir:e,dispatchTaskEnd:t,dispatchTaskStart:r,storageZone:n,storageZoneFileMap:a}),await Lt(o.keys(),async i=>{a.has(i)||(r("."+i),await Rt.remove(vt.join(e,i)),t("."+i))},{concurrency:m}),await we({absoluteDir:e})}import Ee from"fs-extra";async function Me({from:e,to:t}){await Ee.emptyDir(t),await Ee.copy(e,t)}import zt from"p-map";import $t from"node:path";async function N({filePath:e,storageZone:t}){let r=$t.parse(e);r.dir.at(0)==="/"&&(r.dir=r.dir.slice(1)),await fetch(`https://${t.storageHostname}/${t.name}/${r.dir}/${r.base}`,{headers:{accesskey:t.password},method:"DELETE"}).then(w)}import Bt from"p-map";async function I({edgeStorageApiClient:e,storageZone:t}){let r=await It({edgeStorageApiClient:e,storageZone:t});r.sort((o,n)=>A({folder:n,storageZone:t}).length-A({folder:o,storageZone:t}).length);for(let o of r)await Nt({folder:o,storageZone:t})&&await fetch(A({folder:o,storageZone:t}),{headers:{accesskey:t.password},method:"DELETE"}).then(w)}async function Nt({folder:e,storageZone:t}){return(await C({folder:e,storageZone:t})).length===0}async function It({storageZone:e}){let t=await C({storageZone:e});return t.length>0?await Pe({files:t,folders:[],storageZone:e}):[]}async function Pe({files:e,folders:t,storageZone:r}){if(e.length===0)return t;let o=[];for(let i of e)i.isDirectory===!0&&(o.push(i),t.push(i));let a=(await Bt(o,async i=>C({folder:i,storageZone:r}),{concurrency:m})).flat(1);return a.length>0&&await Pe({files:a,folders:t,storageZone:r}),t}import Te from"p-map";import{fileTypeFromStream as Kt}from"file-type";import Ut from"node:path";import{Readable as Ot}from"node:stream";async function M({checksum:e,filePath:t,readableStream:r,storageZone:o}){let[n,a]=r.tee(),i=await Kt(Ot.fromWeb(n)),s=Ut.parse(t);s.dir.at(0)==="/"&&(s.dir=s.dir.slice(1)),await fetch(`https://${o.storageHostname}/${o.name}/${s.dir}/${s.base}`,{body:a,duplex:"half",headers:{accesskey:o.password,checksum:e,"content-type":i?.mime??"application/octet-stream"},method:"PUT"}).then(w)}async function De({dispatchTaskEnd:e=p,dispatchTaskStart:t=p,from:r,to:o}){let n=new Map(r.fileMap);await Te(n.keys(),async a=>{a.endsWith(".html")||(t("."+a),await M({checksum:n.get(a),filePath:a,readableStream:await E({filePath:a,storageZone:r.storageZone}),storageZone:o.storageZone}),n.delete(a),e("."+a))},{concurrency:m}),await Te(n.keys(),async a=>{t("."+a),await M({checksum:n.get(a),filePath:a,readableStream:await E({filePath:a,storageZone:r.storageZone}),storageZone:o.storageZone}),n.delete(a),e("."+a)},{concurrency:m})}async function Re({dispatchTaskEnd:e=p,dispatchTaskStart:t=p,from:r,to:o}){await De({dispatchTaskEnd:e,dispatchTaskStart:t,from:r,to:o}),await zt(o.fileMap.keys(),async n=>{r.fileMap.has(n)||(t("."+n),await N({filePath:n,storageZone:o.storageZone}),e("."+n))},{concurrency:m}),await I(o)}import jt from"p-map";import ve from"node:fs";import Le from"node:path";import{ReadableStream as $e}from"node:stream/web";import Be from"p-map";async function Ne({absoluteDir:e,dispatchTaskEnd:t=p,dispatchTaskStart:r=p,localFileMap:o,storageZone:n}){let a=new Map(o);await Be(a.keys(),async i=>{i.endsWith(".html")||(r("."+i),await M({checksum:a.get(i),filePath:i,readableStream:$e.from(ve.createReadStream(Le.join(e,i))),storageZone:n}),a.delete(i),t("."+i))},{concurrency:m}),await Be(a.keys(),async i=>{r("."+i),await M({checksum:a.get(i),filePath:i,readableStream:$e.from(ve.createReadStream(Le.join(e,i))),storageZone:n}),a.delete(i),t("."+i)},{concurrency:m})}async function Ie({absoluteDir:e,dispatchTaskEnd:t=p,dispatchTaskStart:r=p,edgeStorageApiClient:o,localFileMap:n,storageZone:a,storageZoneFileMap:i}){await Ne({absoluteDir:e,dispatchTaskEnd:t,dispatchTaskStart:r,localFileMap:n,storageZone:a}),await jt(i.keys(),async s=>{n.has(s)||(r("."+s),await N({filePath:s,storageZone:a}),t("."+s))},{concurrency:m}),await I({edgeStorageApiClient:o,storageZone:a})}async function Ke({accessKeys:e,dispatchStoryChange:t=p,dispatchTaskEnd:r=p,dispatchTaskStart:o=p,highlight:n=Wt.white,locations:a}){t(`Validate locations ${ce.format(a.map(d=>`"${n(b(d))}"`))}`);let i=e.map(d=>Gt({accessKey:d})),[s,...l]=await Se({bunnyApiClients:i,dispatchTaskEnd:r,dispatchTaskStart:o,locations:a});P(s!==void 0,'"from" is undefined');let c=s.type==="storage-zone"?G({accessKey:s.storageZone.password,baseUrl:`https://${s.storageZone.storageHostname}`}):void 0,h;l.every(d=>d.type==="local"&&s.type==="local")||(s.type==="local"&&(t(`Index "${n(b(s.path))}"`),h=await j({absoluteDir:s.path,dispatchTaskEnd:r,dispatchTaskStart:o})),s.type==="storage-zone"&&(P(c!==void 0,'"fromEdgeStorageApiClient" is undefined'),t(`Index "${n(s.storageZone.name)}" storage zone`),h=await v({dispatchTaskEnd:r,dispatchTaskStart:o,edgeStorageApiClient:c,storageZone:s.storageZone})));for(let d of l){if(s.type==="local"&&(d.type==="local"&&(t(`Copy from "${n(b(s.path))}" to "${b(d.path)}"`),await Me({from:s.path,to:d.path})),d.type==="storage-zone")){P(h!==void 0,'"fromFileMap" is undefined');let g=h,S=s.path,{storageZone:u}=d,F=G({accessKey:u.password,baseUrl:`https://${u.storageHostname}`});t(`Index "${n(u.name)}" storage zone`);let K=await v({dispatchTaskEnd:r,dispatchTaskStart:o,edgeStorageApiClient:F,storageZone:u});[g,K]=L(g,K),t(`Sync files from "${n(b(S))}" to "${n(u.name)}" storage zone`),await Ie({absoluteDir:S,dispatchTaskEnd:r,dispatchTaskStart:o,edgeStorageApiClient:F,localFileMap:g,storageZone:u,storageZoneFileMap:K})}if(s.type==="storage-zone"){if(P(c!==void 0,'"fromEdgeStorageApiClient" is undefined'),P(h,'"fromFileMap" is undefined'),d.type==="local"){let g=d.path,{storageZone:S}=s,u=h;t(`Index "${n(b(g))}"`);let F=await j({absoluteDir:g,dispatchTaskEnd:r,dispatchTaskStart:o});[F,u]=L(F,u),t(`Sync files from "${n(S.name)}" storage zone to "${n(b(g))}"`),await Ae({absoluteDir:g,dispatchTaskEnd:r,dispatchTaskStart:o,localFileMap:F,storageZone:S,storageZoneFileMap:u})}if(d.type==="storage-zone"){let g=G({accessKey:d.storageZone.password,baseUrl:`https://${d.storageZone.storageHostname}`});t(`Index "${n(d.storageZone.name)}" storage zone`);let S=await v({dispatchTaskEnd:r,dispatchTaskStart:o,edgeStorageApiClient:g,storageZone:d.storageZone}),u;[S,u]=L(S,h),t(`Sync files from "${n(s.storageZone.name)}" storage zone to "${n(d.storageZone.name)}" storage zone`),await Re({dispatchTaskEnd:r,dispatchTaskStart:o,from:{edgeStorageApiClient:c,fileMap:u,storageZone:s.storageZone},to:{edgeStorageApiClient:g,fileMap:S,storageZone:d.storageZone}})}}}}function Vt(e){return typeof e=="object"&&e!==null&&"message"in e&&typeof e.message=="string"}function _t(e){if(Vt(e))return e;try{return new Error(JSON.stringify(e))}catch{return new Error(String(e))}}function Ue(e){return _t(e).message}import W from"figures";import{Newline as x,Text as f}from"ink";import Ht from"ink-spinner";import{Fragment as Yt,jsx as y,jsxs as k}from"react/jsx-runtime";function Oe({feedback:e}){let t=k(Yt,{children:[y(x,{}),e.pastStories.map(r=>k(f,{children:[y(f,{color:"green",children:W.tick}),k(f,{color:"white",children:[" ",r]}),y(x,{})]},r))]});return e.error?k(f,{children:[t,k(f,{children:[y(f,{color:"red",children:W.cross}),k(f,{color:"white",children:[" ",e.story]}),y(x,{}),y(x,{}),y(f,{color:"red",children:e.error}),y(x,{})]})]}):e.done?k(f,{children:[t,k(f,{children:[y(f,{color:"magenta",children:W.heart}),y(f,{color:"white",children:" Done"}),y(x,{})]})]}):k(f,{children:[t,k(f,{children:[y(f,{color:"cyan",children:y(Ht,{})}),k(f,{color:"white",children:[" ",e.story]}),y(f,{children:e.tasks.map(r=>k(f,{children:[y(x,{}),y(f,{color:"gray",children:r})]},r))}),y(x,{})]})]})}import{createAction as T,createReducer as Jt}from"@reduxjs/toolkit";import je from"react";function ze(e){return e.at(0).toUpperCase()+e.slice(1)}function qt(e,t){return Object.fromEntries(Object.entries(t).map(([r,o])=>[`dispatch${ze(r)}`,(...n)=>{e(o(...n))}]))}function Ge(e,t,r){let[o,n]=je.useReducer(e,t),a=je.useMemo(()=>qt(n,r),[n,r]);return[o,a]}var We=T("feedback/epic/end"),Ve=T("feedback/epic/error"),_e=T("feedback/story/change"),He=T("feedback/tasks/end"),Ye=T("feedback/tasks/start"),Xt={epicEnd:We,epicError:Ve,storyChange:_e,taskEnd:He,taskStart:Ye},qe={done:!1,error:null,pastStories:[],story:"",tasks:[]},Qt=Jt(qe,e=>e.addCase(_e,(t,r)=>{t.story!==""&&t.pastStories.push(t.story),t.story=r.payload}).addCase(Ye,(t,r)=>{t.tasks.push(r.payload)}).addCase(He,(t,r)=>{let o=t.tasks.indexOf(r.payload);o!==-1&&t.tasks.splice(o,1)}).addCase(We,t=>{t.story!==""&&t.pastStories.push(t.story),t.story="",t.done=!0}).addCase(Ve,(t,r)=>{t.error=r.payload}));function Je(){return Ge(Qt,qe,Xt)}import{useApp as er}from"ink";import tr from"react";function Xe(e){let{exit:t}=er();tr.useEffect(()=>{(e.done||e.error)&&(e.error&&(process.exitCode=1,process.env.CI&&(console.error(e.error),console.error())),t())},[e.done,e.error,t])}import{jsx as nr}from"react/jsx-runtime";var Ha=D.tuple([D.string().describe(V({description:"Source location to sync from",name:"from"})),D.string().describe(V({description:"Destination location to sync to",name:"to"}))]).rest(D.string().describe(V({name:"to"}))),Ya=D.object({accessKey:oe,profile:ie,sharedCredentialsFile:pe});function or({args:e,options:t}){let[r,{dispatchEpicEnd:o,dispatchEpicError:n,dispatchStoryChange:a,dispatchTaskEnd:i,dispatchTaskStart:s}]=Je();return Xe(r),rr.useEffect(()=>{async function l(){try{a("Validate credentials");let c=await ee({accessKey:t.accessKey,credentialsPath:t.sharedCredentialsFile,profile:t.profile});if(c.length===0)throw new Error("No accessKeys found");await Ke({accessKeys:c,dispatchStoryChange:a,dispatchTaskEnd:i,dispatchTaskStart:s,locations:e}),o()}catch(c){n(Ue(c))}}l()},[e,t.accessKey,t.profile,t.sharedCredentialsFile,o,n,a,i,s]),nr(Oe,{feedback:r})}export{Ha as args,or as default,Ya as options};