From 639e0c786703ca5cd6d4abaa0118b61348376c78 Mon Sep 17 00:00:00 2001 From: Richie Date: Mon, 16 Dec 2024 05:02:30 +0100 Subject: [PATCH] add s3fs class --- .gitignore | 3 + CHANGELOG.md | 6 + README.md | 49 +++--- bun.lockb | Bin 158555 -> 175871 bytes package.json | 9 +- src/backends/local/file.ts | 16 +- src/backends/local/local.test.ts | 16 +- src/backends/local/memory.ts | 4 +- src/backends/s3/create-s3-client.test.ts | 52 ++++++ src/backends/s3/create-s3-client.ts | 2 + src/backends/s3/create-write-stream.ts | 201 +++++++++++++++++++++++ src/backends/s3/localstack-s3.test.ts | 89 +++++++++- src/backends/s3/mock-s3.test.ts | 10 +- src/backends/s3/parse-s3-uri.ts | 19 ++- src/backends/s3/s3.ts | 120 -------------- src/backends/s3/s3fs.ts | 138 ++++++++++++++++ src/index.ts | 3 + src/nafs.ts | 58 +++---- src/parse-uri.ts | 31 ++++ 19 files changed, 609 insertions(+), 217 deletions(-) create mode 100644 src/backends/s3/create-s3-client.test.ts create mode 100644 src/backends/s3/create-write-stream.ts delete mode 100644 src/backends/s3/s3.ts create mode 100644 src/backends/s3/s3fs.ts create mode 100644 src/parse-uri.ts diff --git a/.gitignore b/.gitignore index 383ddd1..ef0604a 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,6 @@ nafs.d.ts nafs.js types + +docs +.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index 153ea37..7d98771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # nafs +## 0.1.1 + +### Patch Changes + +- add s3fs class + ## 0.1.0 ### Minor Changes diff --git a/README.md b/README.md index 3f97f1b..e5bf97b 100644 --- a/README.md +++ b/README.md @@ -19,34 +19,31 @@ await localFs.promises.writeFile('/hello', 'Hello World'); await localFs.promises.readFile('/hello', 'utf8'); // Hello World ``` - - ## Supported File System Methods -| Method | File System | Memory | S3 | PostgreSQL | Logstash | Kibana | -|---------------------|-------------|--------|-------|------------|-----------|---------| -| `promises.readFile` | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | -| `promises.writeFile` | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | -| `promises.unlink` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.rmdir` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.mkdir` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.readdir` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.stat` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.lstat` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.chmod` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.chown` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.utimes` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.rename` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.copyFile` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.symlink` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.readlink` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.truncate` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `promises.access` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `createReadStream` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `createWriteStream` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | + +| Method | File System | Memory | Amazon S3 | Google Cloud Storage | Azure Storage | +|---------------------|-------------|---------|-----------|---------------------|---------------| +| `promises.readFile` | ✅ | ✅ | ✅ | ❌ | ❌ | +| `promises.writeFile` | ✅ | ✅ | ✅ | ❌ | ❌ | +| `promises.unlink` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.rmdir` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.mkdir` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.readdir` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.stat` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.lstat` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.chmod` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.chown` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.utimes` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.rename` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.copyFile` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.symlink` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.readlink` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.truncate` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `promises.access` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `createReadStream` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `createWriteStream` | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ - Implemented ❌ - Not Implemented -File System and Memory implementations are provided via memfs and linkfs, supporting full Node.js fs API compatibility. S3 implementation currently supports basic file reading and writing operations. PostgreSQL, Logstash, and Kibana implementations are planned for future development. - - +File System and Memory implementations are provided via memfs and linkfs, supporting full Node.js fs API compatibility. Cloud storage implementations (Amazon S3, Google Cloud Storage, Azure Storage) are under development, with S3 currently supporting basic file reading and writing operations. diff --git a/bun.lockb b/bun.lockb index 214d948ef27358e6109681cc9268cc204bee450d..e92c0072576619473b882a79915c95118f11660f 100755 GIT binary patch delta 40468 zcmeFacUTn3_dYt^GQyxJpeRWY%z>O#bVS9(h>EUaL=gr-NfH#}U{(|rOKo$`*)?a( zirF=1*PJlxx~BV{4lug<`F`$mzrTC$AKQ=boI0mYovL#xbH_RuwSKBhI zf%m!RtLlV>w*Hpkez^Lr3hl;)I1j#5E!TOf(6n;(q_w9WCZQ#kCJ~1sWEl!^rnbI>kG9^_XpPrba@`S<@pcEALn5a~?pzA@&aHLu?v4(O(6rdN#Ga_(k*QSR0w@`dPD;}!_Eo7O`=z?2M)y`_ zB*c67g0G$74z(;bG6PLesnXNqqD7h(CwiQKOtSw7lsv8nxA4!9*)u6wRNMm20vWN9 zsY(43(P}i4cn44lm0G4TNhxmn$f(#R$fVHRfruNsXgBaBR25a3AxPLjpan%8nQ@7+`joh|R4L*rm7|8`asGylFh=#XfLGAW?19RN zUk6IKJsx(%6gVI>-4r-tZzal{)46P;^)Mx0UB4Nm`x>R9tP|8<eXsS5Ptl=dc*4R7ZRy&jvzH74?>BYfvh8!&fSI9+VuZ1v%OI z<}P`7%TIFTJ$TBW>@PKJG^p6s2EAS%;ODEV5g=8NnwFB5)Z0gO4?I<16e=lFwWPtO z&q#Aij7-ost1XqA4@wo>1*Ha1etcYN8X~H9i|(O{*2hGq$ET&U$g|2s8VzgUQj4C0rHw?t3I6zJ<=0IuG_m4~;5RV8Y>l4Lw?yq`M#SdYa z@;8Cftgxvs*@a{J$b<~l2JmEO2`CzB$n4=uLk-hIR_qK)o}7RI2hhL&#Fb7Yr9c3MI*HCMHznPhJ;C>3kiT+)wXRb zebVDn^eLYq7xP0T`Yl+hKPt{GE-@xa^#nZ4&PbiKur~%Rp;GBG-y%VdC@XPHsvCMW zB{GJ3COuiD36%_eZ6VQQxA>%}$Ta;?7^EI-D;dfRY00e3DjVjulIo|WrAL2E?$%N< z^sTtu%mh#IjO&@0l%iLuuE=JRQ}k)rlT%{T6CxAc;-VGvE0FID{TZM%HIm&Br8Ip? zvRgtVMQC9gsTmo_x2BGcLxKW$p{-Pr?QfEQ{Xr=}4w7Q#H}GUIOV$qsbp)@G^W8wp zgFlU}$OZHzs2%7AP+I7d($k`GBca+9F4EepPkte+AoRon#SsG~l1mH6}?(#U-XO8VBww=p2`vWqk~ zOkePU64fJvDsJ0Ls&E?eDMCGx`lZqw>kqyh_^GlX z1eGcSJQXaBf)t^XC`b*C042{x^_4=s7Cd!91}KF-02B*OW;t2!J?s;I36%6U zfMNy8oNMSW_00qb$m8BJ4FRQ=A08-qQXM=ECTmct;0yYe`uZ-YJ?Of@QUoV~QUyy< zjvD-8h@^iVl*$i-oW@oUIp5F#3986N7958`svsSdLVjSV6p_lqq|n>SRNohqDOFXl z?ka;%nFY$M`Em4%qaJqU*<>gf{XdRcW%U0zdX>?y%m!r^7>{Zi)RPAXwcc`fRpPu# znKf&S)c4rcVb+^FVKL6We;3~!K6OKcGyi8qSiT`Yx#Qk^_3WF^tA>Z25AFK;*qzTG z_iyUnzx4L&$1i=d8RC|8DXlwK;Xo(e{k+Sjbyrgs#FnVrr=mv*$6~R6#rdv0vyd;D zf1&;Un}=srA9G@Tx$9|8+PwF-&#ZLF3~??0e7B)l*%SGVb?tKwx4Hj*!JJEbeJkJj zT=GlxKQk?6>kcd|Vp_iU#@5BR@f*vRysb8?H2d9>h^|97Ewm|O=(>NBeM{}!`p2KE zJ2a@g*MFhU#g_Ayd<@um?m>^`shdXMGF{rRG5>Pjq2|Hks}9&V@pJfgpT{efHtywS z*X-2Q`YW!aL^Y}OA?(aEldTewcOrdh=tGVYT5>0w$y6Gaqr)|njCT4ac9ra!}HG# zy_s>NT1I=*td+uxlxGXAW`x?hCe3c#w%_Hzb=9A{4OwKrK04ziH`%rs7tfm8*0ZmSb0g1=*TewBuCt6qm_ET9loYr2I&Zg}7@qr;yo%GFgs8fZ7#0!7S7+ zP}3i&`jD4EfdI`(aJ57&aB6iCl}gL9Jpwh2kn$GgR&@e2>EP;uQ#0Z=fTJ=dXh?wO zvCN6}6n8NxY!Yi7R+HsAYKlWfEkT1+w#+I>Y`o?oq}Vz$H7v&?z@m~`szK5Tv#-H2 zy)`l$W^S+L-ON}BNP-#50$FRu@<8sIF>?nkUq!=0K=c}x1+oy2bq-q1 z4Q%BUax)gzy%uNALL9aH5ObF0s8xT!X4`~9%r`5>%*$!jql!V7orK^(F_u+MtEq@- z<%_~R3q!CwfNRLI?E}>_k*dQi$_4W0i?b{zEnmWd<$;7*Fmq=uKhc7PfaF`SEN87c z78^qcu_tqpYC$2OUg3+EWFh6X>Mpny_>F}+2Ws+=!m2B0SGHv4E?T~~B?|%BW682W zzFM+85PvIX?yA-3v0Q0UK*hrB1NcQ&EX!5PU$bI)u3B|z?0*5w!X;3xL&}$By9DwE zYnD|(tJwtsZoB1rURyKsidw#ADHa0KyA;a;*-?t+fv8F|^GaI2L1`8Ol3to+Rnn^W zVHNJcvMU9u?XX89Rk^MS675CN5u{p+sj}E*BE(cWQtg>V<+@y1mRF^|2^QNnVksnk z6EzMY)q+yIl^qMIs^y#8u`G}!b}SD>W6#X1X*G?p?0ci#Ma1xpuxDA-wEQ-EmWRAg z$fL2w(d5y5a#X3Xl^qTcOG1Lfo2UCV{RC@LYtSswJp%B?kY___~%~B3``wl6=6B90sJFZ7UF@K zQGsQ7Xf-`6NJADuDi@$x0*=-JQ^d1fH7fb+to6wB$>qW;S#_{S7LcyT7F9a3p6`|3j${%TA2Zk1`Rwx1Zq}Ssak^L#c~6{Q8`|$VHY^5 zT#iF6GcdGdBE%@>TSLl{ilu?0%8Rft=K%iq8q7REtN8|@w0hHu+oq;uS!(TCaMT8I z8PVJ>;7HTjO$ujGu}@lpqj-pGvt|r9ij~w0d%#hXq}IL%M{9{xLqm6Im6Ny>a7~d{ zl5FxC4`yCRtGVDI38l`ji8ZmAXe=kVmKhj|iWJQ=;DSZV6!+3zQZJzG*(ZX$;GEI0}=IvDe^l#digshMrTNfps@DEP3DdRomn z2&Lds0ZR-?YNj|w`3}C!T&vYghEVP{Y?P0|1+kM3ftqT5Qt+g%jsi#BDb=+I98Cnt z#rNRa7v!}-j03@yVpdu#qyEf1NUM1cVZjJf2L_-g#7g=gRS(K4am21AIXLWzaQziH zY8V>e5uouwXHk{NgD0cGQO{xz2Dc9!?WM4S*=C8Ai+Wbttvi4tpE(xRu9gXyI<)6# zULZyOVB&PBWd>7J0^B&Ov19@L9%WI<5q(eZ;(r`Eej+!WPymNh))l|zz*JpW6u}NVap$5Z4Y`uSj z3u4*Mfqb1HmeowFiA5-=(c*fqUJMRVat+knMC!K!lg%5jtmaxiy8+7sInaQahiEnK z=pyO~X)?#k9B#+ZFAKqu_mVTuz|jB&2k$)_NmXJdcm(k2jaZgWtLfZWF-FCwf}=)b z)d2U0%wd*bB3WT|p#Bp(SsezhE^W%1Y@|9su3}cs0h;^ZDBy4#3!(#hiK2-r5zSC= z?i5YUK%}T6u&kq=z2L|)1``(RZ*rbA{{k?QU=l4t8~D-9n0YHLzqgrqgY~f)%W9?7 zw8WB2#Ze{hA|^Cv=B>4wgAmFC3&`w*i{4g!yzmrOsn|} zAuLe$R1Hw4>Qt&m;(gT?9W!sEtIIK}L z^;|Iwa1TM{&{^2KBf?l#xK=$4!dl`j;wd>>TBnL*u5^-=T>{iG;OeqataKZZl6r?K ze+teUS{NneYMJ4lL9%ZZ6rkx0P97RqpJsuRf)R!w-6T#NPBk!N5DfGdqO2Yc4%Hxd zdy$e#WB6%Gwv*N+w7hA6CLA2u!l=Oz%mD{wcucFteQ;DxdJL%4Ub?Ln$CqX(xCrDG z6*X^xqngD#ShZU?-A`Hs1ZoZ&VO_w0vkc780S=ux?5etU?0#ligSzgymqPAr%yNt$yIp zYch8e95qtPtBD&uX-3f^KMowiFX!DT$fMy}p@))3nv=l64`_y=E53p&wAms`T6Qol z5!z|sq+rsf@&p_f61iO4X#9Fm!0iHuQ7Pr|#q=zzhn5f5v%DT!^+P@Gf!WC(fqZZb z3yIRIg&38p8_SNVYt~b$4c&uo?G6ruPPV@R9Qp)Y4wgLASmU~5z@cYl&7I&1%N0kX zq!CHGO)xkF4Vn~(#f2O;E0bQ#JVvWY>Lo3E2uG({CSdd|J1UT`*_-7dH?p@h@I?da zo#4VKTT?t);9+s`g5tN0QKpK+RC38Yx=pgW%eT z^0tX86(UJaV=Qk4MfNCGRsu-;*g3GWhX=#1=Ve+ zbhj| zc4(DL1Si>{ePRb*u5 zEGPB>79+ul)7H4G#YITPX%8vZY5D7_nxo|9G@ghom67?62609ZS^}k7}0<1&gMYI@z;VNDQR2nZ* z1{D|!SOPOdwg2Oc#ODWlTwLF+g+)Ko?Q6w@ji2 z)e0o&B1(!YiNQsb_*KN7Aia$`#Q z#bv#cGPMS!2$YfaiBhtx%>PIYBv3=a5tJ%&k`;*(Umlc1F0z~`C0+4B`4we3QAhAW zGEbE98_N7ID62oXlDM8UfgY(g#RoYSBJ(;D;v!1P7BX*4sh*Z{ek(cuXO#9!`UQ&u z)KS*&#K~g-3GpII4d@C=hP%mfqLl0|(@0rPlnm-+-k4JPp0b=MC1YitC^aaNlh@Nk zSs_VQFs4*Nsx1E*rHay#Ph)J5Y-g~nPn7a$sF5fWAJpR`Wqy>BF`9C55vAl9nI}pO z7!OJ^W)f-R>{d-U<`vD)+@{cn8 zB-1aT_^0}c4>C{`O2nIjQh9Sw^01`LTg&;SK=DsygAdBL2Zg>tRSpR<-~@_)s`65X zssbo^UPYEylW7f5ih#FFYs>OLP-;*kP-;jMS>8;hA##2jP^zyjsG7!K2Qh&`Cuc;+ zd=w~MM5#bDD6P;5vYaSYlq}O!P|8Y|`3z8UV315RLGe#D3?F1~IA{?Xf1@Y?Iu?`) zWXt>{Q2bL(miehNFUWKTC>fe5^Rq$8&ODi407_$K87M`J-bN<-zk|{ivm2DgpK1>h zbP=Tx9tI`FJWw)l2DAj|O;FOmCCl%FQiYE|N&lJ5zmVx`S^gfB24xW_lYUWrkiK~l z#GhJb1wnDpa-dXU1<<|;-9K7K1hEsC^ck=Obwva@X?@DJ{z<$=th~}4oVF; z3~CN~29yTj1&TivxCQ|lsk#A*f2!N`Ayca8j?5D!y}O{)kSCy2@oP}hdjm>_{{qE7 z)hB$A{uh~k1EqWuxKHw;1|-Np30YtXN(D>Fyp2rDg5sabLFUWJ)ESh@(?uQ<=pssv z(IX69M5%#4KFS#W7oTOQ9(tw`J#zu23S9xZh?2(@iNQq_Z8!WcKFdfI)DzAB{E>!2 z-w+_$6yQ{>OusV!KGTT9<$v>82C)^Nk!S?avkYBCY1tV=46Xu7Yam@DpaNrof1ha( zgnyrDClqgIu~B09&OhqU%^gtg z#?A6grk)F2_HbIpHH+HE*oON#*fKfQ*OK&I1wB+Lmr4Havh`~%4|rQIC%N~ccc*8U zSbQpN;B~Jjf9dbsyE`yu{Mo>zdnvB|Nh&dN$y=QT=%ulDxrDOvCEvv zJi9T$mbtCgacb6obvT=jmv(Qg)`ij|p>mn@o@=&WJu&WfLsc=4OMS~v=TDUlU!t;1 zd)cMsdyiFj!lLhY%vij#V(*7n_jxXAI`6$~@Y&&|UZADYxMz_wK{9?wQqjhfIc7N*iHS}u# zeaV-K4ZmLA_Ky-*x3ylrzwSYos^c~;X6?`J9gx#$^1cLzRvRK4S9!p@bjbWRS>teH zbse7}fbK4GC6;gwZY4}dWk0C4`weaofejNaM$j+ zL5T}S*v)U_@MM~4y?Nt4-`?VOdPA$@quc$~-;Ce8kIl)kHH0^davt48>m0ObW}5ei z4v%blZB;EAtzB1P@*gkx{`U;qhTr;Y_l8&JCu9tEE4%%eZiH9v>bLxuqX!QAk9+mz zmGo7dG+^Bg`t7UpvfVSjz|3#;rTMHn+_i!xzgVw-)FocyD|CrKa>Pq7*wT z|PELmhc&{BczC`Kn z?hcz?Sy(^n*ShPy>2Yg1IaR=R#S3e70m~@!5)1TNuu@W<&8A z#;)MA4f9$Q&b4J@@%bCOgU@!X_Tq4^JV+nd`HF{}@j_b^t zFAe9qum$++%0A*Vf`u&$=en^K`0UQO<>6c;>xj=DYy&={m}W&d7tJE^sb|~q8N;kr zhI2hx96n>&K77V8yH(*_FV+X2z1gu<=#}3snBN*5m%xUuL9cAIVE4f#F|W1gl}#3G z>RKJwhus0U4P4`OIxdyvtV6GCwqWnTrLzX>(JNal*pl@+t{-~^?kKo+8+2TMwqOJN z*=oVmzw5YxEbMoTx@{J0Gq}Nw+lWyIPQOvd8Q2DJW42o`n@u`yD2v>L{@G!{4uKoa ztTv;6z@=~2aUNqE z*eh^H!L{3^<7Ti0yWrn`__tff&0=A@;okxH2W}4I_P{@I`aL>s9@_wJ%t83KSH~@2 zk$d6aA@~Pw5wpsLf8f${b=(rR58U*_@Nb`vTgLkAgMUZhAGj6FWk37_H*&v@TgA?T zTYeP&9nf)W*w6#;?-=|8w~l!ognxPP@1Ty`!0v$C2CnfT9k-F?9D;wx;UBootifUU zcLM$$)^S_eD{x1_wL7BYwzCCC;NMC3cT~siWMN0)-zoS9Za3qO!9Q^NV>)gx+W>CN zY513?V$6 z@}!PC&d!2c{s;U!rQ=Srp{L;AdH4tJH1j$Q|1QA4(>m@fy8~_;xW;F6+#f9G4E(zY z|G-^f4bH;9OYrZkj=RKOfjbJW-8mh1g)KM-|1QJ7KXlwR7WN1Hy8{2f-C*2#_yH8^DdZ3jZ$XxH~NJ0{puM|G?d4Ru|zPxb%xU?mpWGZu)ijcS*-RWPL8dzZ>um z++*f)8UBGAd0EFjWoN-HzX|`Y=(y)>=oR>P3;uz7!Mv`*zuWNds*Zcb?tt3{uJJV; z_lD(MgMWA6AGmj{!FBleC;Ypv<36xg;EsZ8cSFa0WD9P!fwL9d+-n3 z7slO!f8g}Dblf+#0o<7T@b9*c=Q!5mc6(Oq0UQKdgk#os+OtPs)9>hbHOKaYo&FFm z{;A_lIhOKgd)Dj`d<3iEnCsp4>@Tn*@9Ows96JYg`D1u_PsdwuY}mc_tiuzy3AQB1 zyzjSXW>4YgeH~}T?%ao;;2J;Baiv(!1Niw2euA@M4IaYJ=kW6(?QXBY9R=6!5$$dZ z9>Gui8Jzkt?QUU@;pYqZ39cODp1@CV`X{uzZ2&jsCH#C!yIbT_^y4eI39bUOdWL=k zm;Q`)w|(HIzlM{~X?N@M9R2vlqD9NpuMKLH>05bHqJXqupk2U-Qkr{rsF>D-PwBa;a+dKF6$z}obeiA0`~=6ZPxq^!t@c_{u>=O%8%fx{*7(_tqxn|inj<8IE!~W zY?dA0AxxjJL4s?*H183n&)6W}({{NX+#_)1KG1d<_W@!0f(;T}Q)c%U!t@o}{$I3R z9s~CmIJb{FY?%E&!oP3u4_qj#_ILQy4jlaZTZc{a%HQFF83(rBCmr_9v7f?)ZNxqY z+m_Y-94^H2VCQ_+VfTCt*0u=P&@Vb{pfkRN3rC6l0=6S-{xw|4Faf*fD{Z15!B$m+ z?fOlJeRRdQaN#o8sTQ1$>o&C`7cPt`3U)WxNI}DauVuxOSPKZ|m_m>)JSKr{2?#>XAm}H|Fyq3x z4B-`#{z7vN$N*shk%7WTB7=l5bCAKp3L--Ut{8|x=tv|}*g#~cpeYVAOo${hT-Z)z zgkWU>GE#^mGD_G-WVB#c0%VNPrv%puKMYGFdMs{7mCM9=_gU3I8}-W0SqW}!dnD&R z657h8@8z4t~;S>rb?d@ZLF1SM}4SwKa`y zZL^H+zJKv-ZuwPF$sapjGYKAaq}0$YhVGS~%uj6GBHh)@-P&cx-(B}#QD@gawl283 zzZ3go`@!3u<9qEnFElYR+1cp)?D#-LaHu74|pBP)#|B&sVdR=4JeRK1&G`z7LlepnIz3`z#g5JSU zE|XEg;jcp5kI(7)a%Yh$@sqx(ryW{Z%{rHB+wRt+)2hGgY2Qo^vY(l7X<3a6vE3gn zbV$mo*mhBMVSdYrPuDK{Y@T6cH{00mjN;lWA1&i~!<$B%)4a0HR$RH{`;cw=TH{>X z!Kc^b4MkztvYF4<|2Zx})#G<=b-2N~T=B~feIC1|pRYVJr>VnPBf}Gn4S!W#UiQwd zZPUz4HR{Gctyg8~o>H%;xlA11^PYc);Oy2G6>Juj@lPdd+qJ`fc+i6KAf<2d z_Cd4WG`v>s_%6<5&`On^eo<4iUCS@uT5<4`O@6n9dyMQ(Dzs}*nXDh1yW;ZH0X0h7 zWqN$>bZKdiw4{-&Oi$NP`;8;_ylnq)>07H8BZltWGJU$^`ipD3uHN*dqTkH6EqZOd z8Ncu9g5(Gz!}Lv%a+$bCjj;%c=ix z_!z^;;ak_vemj1(=H!;45A!zqC;k24!uzFN^2-KGuSZFauLm!tH8M!XO zFDrFM#g2828qCBOGi1vqJM)`{e@m>iY}~}DeEz;@l_!ocsTw8N!*wmh>`>S-ic!XXIQ>NWCW5Z2%oPTDz)%8NH)ozXt zy7yiGHKyXl{X;UZznl<0bZ_mPgrs1 zx&MtW(Y5wkAGl@LCi`X7s#)pH?i9=TRBX19;pxVP+gpcj{8V44nX&KDjP$f6^KGlT zkJz>FP+HnT`z%8h`}lzizMLt&YP-5h@Q%>bFRi@Z*L2$0VNaK;hU^{tJhrXrL33D% z1ikd7TqfzgQc}Y=H(e4Nv8&kp^*0`F_B%a)y7727 z@!Ztw%i-v++_$cdv1{5SI68g(Htp1lvrnze4-fC<@uEspx0)ArEj+&9N!^4`(%Y1h z;aSFpD?Ptg=H=?r$DS?ffd95C(Edv2dW-)&wB};2N7l8DmA!JFnKYUaS#;&01JyIa zV%r+_KAX}ZuXt9EF`wt`J{R+<x)C%u5{e^=O@?Z9|Y|M)61RD^lsJa56c1NV>Yd`YMy=Gy|>-a zjEKjrD-Esd+x^9gxZ)YhPIOH?6|mdL@Z18!uv_69^Ey>F)Zz^9cdPcHG6@woy%h=Y!wn z{86A(j^3r)D9h+BhAO2m@;4z(7wwLeq<8!1C1tvpD8uxk@6^3yA?-?(q_+&cWf^TK z6ao5n`=b2w4XxRfO|J)CfsDko+>(sGXC4RvMT1sKl34>qA)v<7Do6I{8(%Ye?~a7j zE2K#8#TAofvh9$DlUKUd8m&!8wd!mYH47Gk4WK=P|-%W2DQUg}VdgYLQD$7>NGAGDrtkAVe zmeD(TuCi>kBr~YWLwEw9YmKbvf;6>|TEAA7xgt#!Q=`_&vIiBqTxL9F8~*TOTcB|3UC#;23!Yj05n~00k?rW08N#< zz&+qT@BpBntR4Z6fhWLI;2H27$Om2kF9DiSuK`5DAbLa|P>X3AR01jkRe-8MHK00B z1E5DFH^3e606YPjB{VB&ef9HvX2U7#MI1?mHXsgv+$jA#ZH3}7fQ z92fzN6#NEo9#*}O^Z|SUKfqt`8pxG0P>)g%QjgJOrO8R-nTF~G^gd0>%fM!gn61E0 zfZpV_21)^?0UMwUU<=p-#ek9ky%PKZ_yimU=-uTq0KF_d7{LEqWl+(=FTTzVN^joM zTe{Z)nmaV!X)M#wr4?W_FbwDi3;_B7t$;9~4L}n#6le-G1DXSkfMB38&=3d$8UU-{ z;A#rk8YI>N^hZxKfmy(8fR>OkKyM%(hy(P17N`#d0S$n-+7NlcTLCMm5h zG?{7jp!r6t11$yz;H>y&Hoeq56rk6`Z-U+e=0af}FcPh#Sw0Ae19}4qKt~`7hz8mL zEdd=60_39HK43rK3cdnx1nHx|F(3~(4$z`-5-^;?$7$d&unyP&ECH4RivU_P#{yZv zIAA=G4NL$g0+WCoU^2jfDZo@<8Xy4Efdn85hz1%0!9Zi6JPp{}@OVC$QD|ulkO*`I zB7g&sZvnOfJAqxmZeR~UZ^``)d;&fLUx9DH8{jSQ1b7NO10Dfqfpfqgz{b2HH&HdF+3{8)VW;?6jGh0%m{)Fb8Pgr2qAe_R9Of z1K=S*`{bX%T9i!zS_5qX+81e;A(Tfs+Un^2c6#@n-e<1>V8@9Px0v=Q5Dt(s{owgXzd_5ts@Rb^0LC6!C0{CLgWer0u_M@KzV>lH<4>bBa0v&*GfOJT=IcR5~ zH_!`+17d-mKn$P!eW)uthl3M2qYKq8O~^Z`~i091G&Fc`QF2*6Yz6Bq_i!-vXrB2KrXNs*bb}%)&OL9x=dGtE&^5ni-G09GGHz+2bcxSps>#bW&=wA zGC+kE0tPTY=rc4qzv+3)lnf15N^l z3-~wz90iU5hk*mYL4cGG0TfR1kQ#RkI1c0iWPs!pQiYyE`W$c`I4#pZK+gha0Ln8e zXAt*C^yiLLHD- z95fZw95fzO16m9q@5xKrylAH+o+6<{iZ&~f8AyqCFp2=}VkD#riKh@#AzRP{(A$6= z(zNZ;9zgkuVX{NrLtUeEgOaCo6?Ks!BYoI36bPt73csSP^dU8XBe>vRfOgF!ARg!f!~k?384W}M&4DIB47^184+z15JTYfNoCf1AzcN!qfsh0XKjiiO5@eB%-pECQ#Zy33plO zVU$TNq)JJNDk2>(fD9@INT%>)oEqp0D3uWP0~CElZlq@rjgtaBd?^Yv6bpb_|B1#RQGM?1`l&KgXB?>zkP%5HOQ%FfK7*x^yky1Iu zFm(%+QM!Q25|oBfS+ZM}=D*@0JzOXik+QJ|ElAnO2vtN8Q#@4qoOD`2t~7*rs!Zu7 z<3^KA?0*s(_hmTv_JE>H{jXHs(MaCODBW33(-ZuQTG{pPI4+wvx4-ACmpgu(ekP2Jp>pG3<5}}KS0ZK3eX3jv62Lk-58)ZK+CbR6w~7N z8_I$4(eYWtpw zd;=9^vLY2EBg&plX+=iVSXbE-6dh{JJYWtm8zA{iUtxp->Qdsn{nRm-U04jo+>}##yHN0Yb02W=j^x^LXGjjpx|16P=i(_RQJ|-FCPb>Gb}Of6Ajb#3`r_#vImOWgpOGbS^H<>N4%p(7=+)0g_#q$*?eb>P-`OR&1=nt_=%jY zqjEOi2h(GHyKi3IL2g0-zO*qH=0n3@Iajd5>MpNc;@g~;EAqzI0T_Q}5RIf6BOWbw z%Dq!q_LjMjKap#}KQb5UPC_m6S%jIUvvW>V82(_PtO-ZA6cdu6!K;f4*KsU zbto?EA$h%G!sSV5uPk?LVj=ZSu`6$K*?+lXtp_s_6Q2bJO zSXzye{Q8o@?a9B6s+X@yIlr-OREdiQtmnxw)=yn|4&%Yy2YtD;l<*ZL{gnf}ay&X^SKeH&HqQ<5#>Dq_ucaKWs7b1O z_(|BzoyhTX_x8u2Usp(w4c17)} z$!+Xj&oiO=#dSqKVlneo^Ojv#7Oy|NFz0z`;S`ls4rQFX=4Slg-PFTRF|pbKzwL2E(G;{U1mB+r+La%vtCtbU|51<>XDrg=>edzjQpNP~*CzFbNv`Mpxk`X&!LJw@Zlb z-kP(n2DY;ep*}~O+fuCR*?FkUH50-{+<;19-6e1WBpK$)2#O< zcSD|~A703gz`X?J+^Ha}oq^hXDhfx*mQy9+Gtotr1kai9Q8_nrv}v`up^JWBCf4pn zcLy8gc8!akvSHWNW4CzRq0q>}t!ZUp$V`ljBUPk(j^9#m?YY`|P-U@Jar3xZMK}TL zj>_4UH{wk??b`jOA~a}tVkJ>moi+>2>Rwf_pM~c1t}3*gh2{EdRbe*CL#qi}Aa}&6 znBvg8*s{s8rxWk^!T_}b2F_O#g648IY9wz~6P#wl7v<>9J#FF+@L&I!B-e&WDu;6( zH+x(;q-EZTLNluBLOjYkD#v;*nKt*Cx_tKrqK0@oV29UzXJfll4)P4Lxi!7cUbDf4 zHX7FyT<35x{>rI~7dNiUDl;V@7a9RlW0iv?KdpVzFrnv{4CG*0q8W16UDz`Rj=y&o z)-FXml*2NQtx9RvYK2(|X!;vF?9EG#+w!$Mgb8!ub+CtYN8t0}Ntbopa#NI$W2_v^ z*`j9Ujf*O~cq0e94c%jQ^$@P3tfO*-XU{s?mgiGH(;W!bTng_%55aOCtdE0rD~!&$ zr7coQj2^WG8fZC|Yn5`YXm{(;mDWxkFhaJD9cYb*5J6>?lSxb3J$Lt;9!~om-LYX* zDrc2?7RgS_iH{V-vSQADPhs^u3<%lF|KQz9*uhqunQx+6eI!48Eo{`1A_W`n^HINY z!s$2PJ}sNAdSVF;y1hj?=mGUN|Xr2>trpET!{Q&N9>IImsg@5`Sd z2Xl|=PpT!HA{)o636EfdZ&q7yT!`MDk6BIYxE1qyb9U*SzfppgX_UBITWGxy?W!0k z-P^ca^N6St;qs8@XgHA9n*xQou;H(qM7k}*XYrT(RWu7I>QGS*JFRum-|j@|m(GPb zTwUQA%JL3%g`$h#lXASO?TxCKCnV<`PsaPp$&qroNC%`km_wZ$bT)V^)V>Y+w0qda#vP zEw)K4to6m;eKY%?Jo2o}z%c44d1qTvU+6-%H`EvUF8Oucj@}KVpfC15wyemhfSIr$ zuV8@<1nZ@62{%vOmm>Je;jI@-)K0Xv(KUyrJSe30x8OH45Oa#%}<9*+}qQ z2C4}bK5s+^E2oCOxD;|NIsbhTSfIIz4t5I`x|4=-e(2eK&5MlM=IsIvJY3PL9v3Wl zZi20hV4?Lo(5zq~m+F`iJZ(7&uE2AhGse!vulX}OwRD;ynil8M(O^MKidTY#meAy% z1PhaX2mKZ-3|kKO%o_`5mSc6aYb^c}o_RUNar(S1Av8YZ#wmx*rjBj)b<>iHgOCHu z)a<~AHF9X}g=3Ga z3Hd9~4vfCKE77ivO@-B~LBS`lgzdr2gvFr#${Ds-qDrhDeyYbN+<#J6V-PfKF0HU1 zEnGL-1kHcRbN;dfJ{Dhv=qTsfmUF#RAy?zN9ko-}VOBH@5rS5s_QpD40yO=VgJ_30 znb2+f&XU&(eO1nq#UV5?y+cRPszF@?8<9HUI&3)p^WHfEH_q7TRkC&dtxkwojiIC* zP+Th{FW-BXJ?%HX(m;^CcT^6_Z69{LWu)b%BhZkarZu6$?bTAe+}D7Hv=jyro!d&t zTf=4Y@9l&ZYvHqUX6~tbyOXC+c!&D~r7vc-7BZp1%OgXWy^gaNPORm86-mJu`C-sG zE^H(1HaTl6nh*5aMI!-Ez0|)(wtRW7#=^OE=sHE{w4Sr~|Me*P^|X-3s=sm!@8XH4 z_OIY4N>66u=vEFef0}S`R)>dS$Hho`V*69cW71JMy7yJus~S0-?zE+69&u4s_5V$H zxgLG49OC=?;d_qu&o*}uz4D^5`RkS`vr1~AcfpjGr}_`mQ;CEU*#CJYD46FzROI+; zhridfrCzY!NP`VagEf|hU#~j<^Wq_Qgm8ExmUE>dWmy{;F24Mdxg)&#qqHw|tP4kl zJbwO*)k#^Emf=qXH_>t(E}YneQ7x}9j>x6dnkx97{?YCsqK#V*+R6>?3W3ZPa|7==9V{~9)@iEDzc#*kRMqPwoPh=((n(0Z2~Fkn-CBFgZ+^#K^g&`wC*ZL`_`)&Tj4>#&@qK&-**z?QI;>)Sy+1&G@!FE zpKK|I2zxkHsU4r|IjXRPa*S}xgPNz|56guY=1l4=qO;(63YuR)uYs2CA|!7`t;!k5&n}nTHfi#*FNGzPQ<8)9hfa5nyjZp{N8d%* zgR=afF2aOcs8u;v`TfDxQ~Gw<86oyQ7HA*UiY`L&ZO~9oa;|W=OQXmli|PA8@vhNZ z^|Ff)L>kH=&<=Zch6P`5Fs*QyRp=_5pceaf6(&HFZ_!n_PPHn>N>^(8mt~(7-_jME zo~pF2)2>5N8lSfO)UJZ%cJOPu3N1-~sH+fh8}tS!$v<`#@}GcOM@%~n(!HZ_hSaJ= z2$S}L`bG%1Nv%nQFn1Sd#|Xjw4rqVS9iY=9g!mhv+aiQPB)=3P%qRYNgphm?)TEoR zp7hFe6Ox~TR_i8Y9tEw_P58P4tHduR7#32g1>VA>!2F(@h zcVYXGqgddI(srfIQXyMDB~oxYQ_u$4vXUt^>_0U~j!=3JA$Jd~C{0pod)Py$xCcvz zBLAs+K_G?Vd*PNxv~c_IPi!eRlulCk^?Jc`AM}sug){sAQO>)dZe@(Vj}boSLQk=y zl$UzhMvcWwHtk3UpwlX7mjr$E2=af@02y$LO)HQZ`8$ws$i5T%YZQZ7+TRQN$7IIXcWFp z63XWp@!gVz%nJp4!Th61YtOGu7J{B36HD{UJcL#0LWvjKHsKlMn4e3JOEdoJaii(= zW93SDWm8a={O`NVTZlM`%9XuNQBw8=c}@Q@zKWXCwzi(<+|)&|D?5KLpKjRA1@I%`eGeh}VS{XQVBXzH0a}6h9dJ(f=Q2 z&<}fsJS~;RDdAQ6;)lZGit&Tv-Ua**^G30$c=#*+hhp-$_Mh8dx-V$|E77L^}E@{if0wEVl`1%CgBuKGp2a#W?M z>M~H!UdG*0gMs3k=9v+fPh~o3E;T8n>&q+8^UjVOtssN7W!8z_uNS;wWa zMrDU3neI`#s6vgy1BF~@D1GGkK1Q+?-Sh66WlL7Q5r?(6yFXqh7I!~!Mjg{ndt9)x zc>iNQ8)?dY>;L18uXq#fUmN$c|MkstK{d)P@~>}mf4IHnl~qZ;RaKn*@rKm#*Egif zmafb#<)%luL6mO{`Tu|3{wa4?|8uOB3G#1@|L@*nD>44XNXIkYk9#g24m1D6* zS)gz6CTTefh5GXA@jm-;5^q4@&?ZaChNB_rG`)mTPzX zclQ3b4{yr-L_xpFkJG>I4MkJlIvjt!b^O1wLHy%u4|(;+oKRY-j4)+gRi2BDe3UoW zA8%izr-=V%6Z+qrXg+a?vHFy_v{b8^Ps-ZfI^UCCnZP%UcuG4kRC>?q_;YoK29@{v zi<6YTj}lr<lEpz9&2R)XjZW@hj`d%I1C|8zq7W!OlId93w05M1WuKAz8 z01c5>2ufA?j?$N~tNe}qcNZGp79;2$HM}6g) zMQ-nR%HmM@xixZBZmaYqfN{b95Ju^lmYOBqvz0ONR!NV~lrn0a^py=MN)HxgI4@zt zPR>Do#FCo(%ZKWpd;MJqBu1JTRLwtH@e^%hC*|*=sAyrFj2!s>F`d-pfkB~@*P>#{ z6?uFuiZ?AQ@xt+ETz$dvIp@iB7aBb0oDC!Fq~GVhj`CT&W@qU!sr~2_V*HKZhTJq=qQAJo3{D+dTlLD1~e(b7$?X7$pe zhLlvj>O@2Bj!#WH=kl$j2ZW!_y-nE-}t6H8!qyTtZ|@@93m{iEeReBut1*i;ATm ze=^Ds8dTm7D}-CNiBQgnbCX_2B{$;Z5_`wR^hcAE(-ZqgB}EITYw^ydl-D>> zPDu)r%JP-$WG%gWuTCHm!r97v1-rsJkW5zGUmCYWTy4IB6}@ju@cy9 z&}@BT&$vXrTT)_N&-9e^gvcNzgE~32fa{xzXz1P2^ciXBN#Se_-qHORnR?Xuy}I5F zFQW=p&G`z|X=I>Q(Kz%(DmBYnY?hP}8=0CW4E@I8C8p0@Q5RyNOvH`8L6SU>RMQjV zjCD(jJd{Q1<|AYxT3!V${n1+}{wNY#vo$}TJ6_5>H7PwMO7E5snJf;vtq*xV*=%dA z;(VIFi_u(x6(bH!6kom;_BG=jg$3>SQo_n6yrn~GLR?yGf48XkICN}kT8ch0!L3J1 zQomGviqPGQcM@**rs`Y5?$oxc-N zcYZGrxR+cd=kKjZtuQhx?0Lsk63Tz#EIA(`_Z^1sZ%Ffg?t)iWzJky?ls6TIx8}rHrZwlS<)ko4$5&U1x@cf~3*J@e(VVX;)S-{V z?eS40l&>j7z2eFX<=gP3g)6Q3az-Vb!g%-pYmBBKJ9MV&`2lsq4C9zy8VHOo7?*uI z&?=tk_XB}eA;d!bnM`3$0XmCy`bnTUFoBK!Oxn{s{eeyZTEacuI0zV}2(6VtOaefQ J_h&K90RTid8AkvB delta 31081 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 U=v8KgjdNz3>mu}pNMq6e0`1kaz5oCK diff --git a/package.json b/package.json index 168b7f4..fe0d197 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nafs", - "version": "0.1.0", + "version": "0.1.1", "description": "Node Active FS - nafs. File system abstraction to read/write files to AWS S3 or local directory. Includes middleware for usage with express.", "main": "dist/cjs/index.js", "module": "dist/mjs/index.js", @@ -15,7 +15,8 @@ "scripts": { "start": "bun run src/index.ts", "test": "bun test", - "build": "rm -rf ./dist ./types && tsc -p tsconfig.build.json && swc ./src --config-file .swcrc.cjs.json --out-dir ./dist/cjs --ignore **/*.test.ts --strip-leading-paths && swc ./src --config-file .swcrc.mjs.json --out-dir ./dist/mjs --ignore **/*.test.ts --strip-leading-paths && ./patch-dist-dirs.sh" + "build": "rm -rf ./dist ./types && tsc -p tsconfig.build.json && swc ./src --config-file .swcrc.cjs.json --out-dir ./dist/cjs --ignore **/*.test.ts --strip-leading-paths && swc ./src --config-file .swcrc.mjs.json --out-dir ./dist/mjs --ignore **/*.test.ts --strip-leading-paths && ./patch-dist-dirs.sh", + "docs": "typedoc src/index.ts" }, "publishConfig": { "access": "public" @@ -46,7 +47,9 @@ "@changesets/cli": "2.27.10", "typescript": "^5.7.2", "@swc/cli": "^0.5.2", - "@swc/core": "^1.10.1" + "@swc/core": "^1.10.1", + "@aws-sdk/lib-storage": "^3.709.0", + "typedoc": "^0.27.5" }, "dependencies": {} } diff --git a/src/backends/local/file.ts b/src/backends/local/file.ts index 406ac65..5d79a90 100644 --- a/src/backends/local/file.ts +++ b/src/backends/local/file.ts @@ -1,12 +1,18 @@ import { link } from '@ricsam/linkfs'; -import type { IFs } from 'memfs'; import * as path from 'path'; -import * as realFs from 'fs'; +import * as realfs from 'fs'; +import { parseUri } from '../../parse-uri'; -export const createFileFs = (uri: string, fs?: IFs): IFs | typeof realFs => { - const lfs = link(fs ?? realFs, [ +export const createLinkFs = (uri: `file://${string}`, fs: T): T => { + const parsed = parseUri(uri); + const lfs = link(fs, [ '/', - uri.startsWith('/') ? uri : path.join(process.cwd(), uri), + parsed.path.startsWith('/') + ? parsed.path + : path.join(process.cwd(), parsed.path), ]); return lfs; }; +export const createFileFs = (uri: `file://${string}`) => { + return createLinkFs(uri, realfs); +}; diff --git a/src/backends/local/local.test.ts b/src/backends/local/local.test.ts index 43e79f9..d355d9f 100644 --- a/src/backends/local/local.test.ts +++ b/src/backends/local/local.test.ts @@ -1,21 +1,21 @@ import { expect, test } from 'bun:test'; -import { createFsFromVolume, memfs, Volume } from 'memfs'; -import { nafs } from '../../nafs'; +import { createFsFromVolume, Volume } from 'memfs'; +import { createLinkFs } from './file'; +import { createMemFs } from './memory'; +import { toTreeSync } from 'memfs/lib/print'; test('memfs', async () => { - const fs = await nafs(':memory:'); + const fs = createMemFs(); await fs.promises.writeFile('/test', 'test'); expect(await fs.promises.readFile('/test', 'utf-8')).toBe('test'); }); test('fs', async () => { const vol = new Volume(); - const realFs = createFsFromVolume(vol); - const fs = await nafs('file:///tmp', { - fs: realFs, - }); + const memfs = createFsFromVolume(vol); + const fs = createLinkFs('file:///tmp', memfs); await fs.promises.mkdir('/tmp', { recursive: true }); await fs.promises.writeFile('/tmp/test', 'test'); expect(await fs.promises.readFile('/tmp/test', 'utf-8')).toBe('test'); - expect(await realFs.promises.readFile('/tmp/tmp/test', 'utf-8')).toBe('test'); + expect(await memfs.promises.readFile('/tmp/tmp/test', 'utf-8')).toBe('test'); }); diff --git a/src/backends/local/memory.ts b/src/backends/local/memory.ts index a8493b1..e959a29 100644 --- a/src/backends/local/memory.ts +++ b/src/backends/local/memory.ts @@ -1,5 +1,5 @@ -import type { IFs } from 'memfs'; -import { fs } from 'memfs'; +import { fs, IFs } from 'memfs'; + export const createMemFs = (): IFs => { return fs; }; diff --git a/src/backends/s3/create-s3-client.test.ts b/src/backends/s3/create-s3-client.test.ts new file mode 100644 index 0000000..3770973 --- /dev/null +++ b/src/backends/s3/create-s3-client.test.ts @@ -0,0 +1,52 @@ +import { + CreateBucketCommand, + GetObjectCommand, + ListBucketsCommand, + ListObjectsV2Command, + S3Client, +} from '@aws-sdk/client-s3'; +import { beforeAll, beforeEach, describe, expect, it } from 'bun:test'; +import { S3Fs } from './s3fs'; + +describe('s3Fs LocalStack integration', () => { + const LOCALSTACK_ENDPOINT = 'http://127.0.0.1:4566'; + const BUCKET_NAME = 'test-bucket'; + let s3Client: S3Client; + + beforeEach(() => { + process.env.AWS_DEFAULT_REGION = 'us-east-1'; + process.env.AWS_ACCESS_KEY_ID = 'test'; + process.env.AWS_SECRET_ACCESS_KEY = 'test'; + process.env.AWS_ENDPOINT_URL = LOCALSTACK_ENDPOINT; + }); + + beforeAll(async () => { + // Create a client for test verification + s3Client = new S3Client({ + endpoint: LOCALSTACK_ENDPOINT, + region: 'us-east-1', + credentials: { + accessKeyId: 'test', + secretAccessKey: 'test', + }, + forcePathStyle: true, + }); + + // Create test bucket + try { + await s3Client.send( + new CreateBucketCommand({ + Bucket: BUCKET_NAME, + }) + ); + } catch (err) { + // Bucket might already exist + } + }); + + it('should connect', async () => { + const command = new ListBucketsCommand(); + const response = await s3Client.send(command); + expect(response.Buckets?.map(({ Name }) => Name)).toContain(BUCKET_NAME); + }); +}); diff --git a/src/backends/s3/create-s3-client.ts b/src/backends/s3/create-s3-client.ts index d2b18ed..82aaaac 100644 --- a/src/backends/s3/create-s3-client.ts +++ b/src/backends/s3/create-s3-client.ts @@ -32,5 +32,7 @@ export function createS3Client(uri: string): S3Client { process.env.AWS_ENDPOINT_URL_S3 ?? process.env.AWS_ENDPOINT_URL; + config.forcePathStyle = parsed.forcePathStyle ?? Boolean(config.endpoint); + return new S3Client(config); } diff --git a/src/backends/s3/create-write-stream.ts b/src/backends/s3/create-write-stream.ts new file mode 100644 index 0000000..e098831 --- /dev/null +++ b/src/backends/s3/create-write-stream.ts @@ -0,0 +1,201 @@ +import { + S3Client, + CreateMultipartUploadCommand, + UploadPartCommand, + CompleteMultipartUploadCommand, + AbortMultipartUploadCommand, +} from '@aws-sdk/client-s3'; +import { Writable } from 'stream'; + +const MINIMUM_PART_SIZE = 5 * 1024 * 1024; // 5MB minimum for multipart upload + +function concatenateUint8Arrays( + arrays: Uint8Array[] +): Uint8Array { + const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + + for (const arr of arrays) { + result.set(arr, offset); + offset += arr.length; + } + + return result; +} + +type State = 'uploading' | 'initializing' | 'error' | 'finishing' | 'finished'; + +export function createWriteStream( + client: S3Client, + bucket: string, + key: string +): Writable { + let uploadId: string; + let partNumber = 1; + let buffer = new Uint8Array(0); + const uploadedParts: { PartNumber: number; ETag: string }[] = []; + let state: State = 'initializing'; + + const updateState = (newState: State) => { + if (state === 'error') { + throw new Error('can not transition from error state'); + } + if (newState === 'uploading') { + if (state !== 'initializing' && state !== 'uploading') { + throw new Error('can only transition to uploading from initializing, not from ' + state); + } + } + state = newState; + }; + + const stream = new Writable({ + async write( + chunk: Buffer | string | Uint8Array, + encoding: BufferEncoding, + callback: (error?: Error | null) => void + ) { + updateState('uploading'); + try { + // Initialize multipart upload if not already done + if (!uploadId) { + const { UploadId } = await client.send( + new CreateMultipartUploadCommand({ + Bucket: bucket, + Key: key, + }) + ); + uploadId = UploadId!; + } + + // Convert chunk to Uint8Array + let data: Uint8Array; + if (chunk instanceof Uint8Array) { + data = new Uint8Array(chunk); + } else if (typeof chunk === 'string') { + data = new TextEncoder().encode(chunk) as Uint8Array; + } else { + throw new Error('Unsupported input type'); + } + + // Append to existing buffer + buffer = concatenateUint8Arrays([buffer, data]); + + // Upload parts when we have enough data + while (buffer.length >= MINIMUM_PART_SIZE) { + const partBuffer = buffer.slice(0, MINIMUM_PART_SIZE); + buffer = buffer.slice(MINIMUM_PART_SIZE); + + const { ETag } = await client.send( + new UploadPartCommand({ + Bucket: bucket, + Key: key, + UploadId: uploadId, + PartNumber: partNumber, + Body: partBuffer, + }) + ); + + uploadedParts.push({ + PartNumber: partNumber, + ETag: ETag!, + }); + + partNumber++; + } + + callback(); + } catch (err) { + updateState('error'); + callback(err instanceof Error ? err : new Error(String(err))); + } + }, + + async final(callback: (error?: Error | null) => void) { + try { + updateState('finishing') + // Upload any remaining data as the last part + if (buffer.length > 0 || uploadedParts.length === 0) { + const { ETag } = await client.send( + new UploadPartCommand({ + Bucket: bucket, + Key: key, + UploadId: uploadId, + PartNumber: partNumber, + Body: buffer, + }) + ); + + uploadedParts.push({ + PartNumber: partNumber, + ETag: ETag!, + }); + } + + // Complete the multipart upload + await client.send( + new CompleteMultipartUploadCommand({ + Bucket: bucket, + Key: key, + UploadId: uploadId, + MultipartUpload: { + Parts: uploadedParts, + }, + }) + ); + updateState('finished') + + callback(); + } catch (err) { + // Try to abort the upload if something goes wrong + updateState('error'); + try { + if (uploadId) { + await client.send( + new AbortMultipartUploadCommand({ + Bucket: bucket, + Key: key, + UploadId: uploadId, + }) + ); + } + } catch (abortErr) { + console.error( + 'Failed to abort multipart upload:', + abortErr, + 'when handling error:', + err + ); + } + + callback(err instanceof Error ? err : new Error(String(err))); + } + }, + + destroy(error: Error | null, callback: (error: Error | null) => void) { + if (state === 'finished' || state === 'finishing') { + callback(null); + return; + } + if (uploadId) { + client + .send( + new AbortMultipartUploadCommand({ + Bucket: bucket, + Key: key, + UploadId: uploadId, + }) + ) + .then(() => callback(error)) + .catch((err) => { + console.error('Failed to abort multipart upload:', err); + callback(error); + }); + } else { + callback(error); + } + }, + }); + + return stream; +} diff --git a/src/backends/s3/localstack-s3.test.ts b/src/backends/s3/localstack-s3.test.ts index c3557a4..cd7810c 100644 --- a/src/backends/s3/localstack-s3.test.ts +++ b/src/backends/s3/localstack-s3.test.ts @@ -5,7 +5,7 @@ import { S3Client, } from '@aws-sdk/client-s3'; import { beforeAll, beforeEach, describe, expect, it } from 'bun:test'; -import { createS3Fs } from './s3'; +import { S3Fs } from './s3fs'; describe('s3Fs LocalStack integration', () => { const LOCALSTACK_ENDPOINT = 'http://127.0.0.1:4566'; @@ -63,7 +63,7 @@ describe('s3Fs LocalStack integration', () => { describe('root bucket operations', () => { it('should write files to root when no prefix provided', async () => { - const s3fs = createS3Fs(`s3://${BUCKET_NAME}`); + const s3fs = new S3Fs(`s3://${BUCKET_NAME}`); await s3fs.promises.writeFile('test.txt', 'Hello World'); const objects = await listObjects(); @@ -74,7 +74,7 @@ describe('s3Fs LocalStack integration', () => { }); it('should write files to root with trailing slash', async () => { - const s3fs = createS3Fs(`s3://${BUCKET_NAME}/`); + const s3fs = new S3Fs(`s3://${BUCKET_NAME}/`); await s3fs.promises.writeFile('root.txt', 'Root file'); const objects = await listObjects(); @@ -86,7 +86,7 @@ describe('s3Fs LocalStack integration', () => { const PREFIX = 'my/prefix'; it('should write files under specified prefix', async () => { - const s3fs = createS3Fs(`s3://${BUCKET_NAME}/${PREFIX}`); + const s3fs = new S3Fs(`s3://${BUCKET_NAME}/${PREFIX}`); await s3fs.promises.writeFile('nested/file.txt', 'Nested content'); const objects = await listObjects(PREFIX); @@ -97,7 +97,7 @@ describe('s3Fs LocalStack integration', () => { }); it('should handle leading slashes in paths correctly', async () => { - const s3fs = createS3Fs(`s3://${BUCKET_NAME}/${PREFIX}`); + const s3fs = new S3Fs(`s3://${BUCKET_NAME}/${PREFIX}`); await s3fs.promises.writeFile('/absolute/path.txt', 'Absolute path'); const objects = await listObjects(PREFIX); @@ -107,7 +107,7 @@ describe('s3Fs LocalStack integration', () => { describe('read operations', () => { it('should read previously written files', async () => { - const s3fs = createS3Fs(`s3://${BUCKET_NAME}/read-test`); + const s3fs = new S3Fs(`s3://${BUCKET_NAME}/read-test`); const testContent = 'Test content for reading'; await s3fs.promises.writeFile('read.txt', testContent); @@ -117,7 +117,7 @@ describe('s3Fs LocalStack integration', () => { }); it('should handle binary files', async () => { - const s3fs = createS3Fs(`s3://${BUCKET_NAME}/binary`); + const s3fs = new S3Fs(`s3://${BUCKET_NAME}/binary`); const binaryData = new Uint8Array([1, 2, 3, 4, 5]); await s3fs.promises.writeFile('binary.dat', binaryData, 'binary'); @@ -130,13 +130,84 @@ describe('s3Fs LocalStack integration', () => { describe('error cases', () => { it('should handle reading non-existent files', async () => { - const s3fs = createS3Fs(`s3://${BUCKET_NAME}`); + const s3fs = new S3Fs(`s3://${BUCKET_NAME}`); expect(s3fs.promises.readFile('non-existent.txt')).rejects.toThrow(); }); it('should handle invalid paths', async () => { - const s3fs = createS3Fs(`s3://${BUCKET_NAME}`); + const s3fs = new S3Fs(`s3://${BUCKET_NAME}`); expect(s3fs.promises.writeFile('', 'content')).rejects.toThrow(); }); }); + + describe('s3Fs.createWriteStream with Uint8Array', () => { + it('should handle Uint8Array input', async () => { + const s3fs = new S3Fs( + `s3://${BUCKET_NAME}?endpoint=http://localhost:4566` + ); + const writeStream = s3fs.createWriteStream('uint8array.txt'); + + const data = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" in ASCII + writeStream.write(data); + writeStream.end(); + + await new Promise((resolve, reject) => { + writeStream.on('finish', resolve); + writeStream.on('error', reject); + }); + + const content = await getObjectContent('uint8array.txt'); + expect(content).toBe('Hello'); + }); + + it('should handle large Uint8Array inputs', async () => { + const s3fs = new S3Fs( + `s3://${BUCKET_NAME}?endpoint=http://localhost:4566` + ); + const writeStream = s3fs.createWriteStream('large-uint8array.txt'); + + // Create a 6MB Uint8Array + const largePart = new Uint8Array(6 * 1024 * 1024).fill(65); // Fill with 'A' + const smallPart = new Uint8Array([66, 67, 68]); // "BCD" + + writeStream.write(largePart); + writeStream.write(smallPart); + writeStream.end(); + + await new Promise((resolve, reject) => { + writeStream.on('finish', resolve); + writeStream.on('error', reject); + }); + + const response = await s3Client.send( + new GetObjectCommand({ + Bucket: BUCKET_NAME, + Key: 'large-uint8array.txt', + }) + ); + + const content = await response.Body!.transformToByteArray(); + expect(content.length).toBe(largePart.length + smallPart.length); + expect(content.slice(-3)).toEqual(new Uint8Array([66, 67, 68])); + }); + + it('should handle mixed input types', async () => { + const s3fs = new S3Fs( + `s3://${BUCKET_NAME}?endpoint=http://localhost:4566` + ); + const writeStream = s3fs.createWriteStream('mixed-input.txt'); + + writeStream.write('Hello '); // string + writeStream.write(new Uint8Array([87, 111, 114, 108, 100])); // "World" as Uint8Array + writeStream.end(); + + await new Promise((resolve, reject) => { + writeStream.on('finish', resolve); + writeStream.on('error', reject); + }); + + const content = await getObjectContent('mixed-input.txt'); + expect(content).toBe('Hello World'); + }); + }); }); diff --git a/src/backends/s3/mock-s3.test.ts b/src/backends/s3/mock-s3.test.ts index 85f41c1..e573508 100644 --- a/src/backends/s3/mock-s3.test.ts +++ b/src/backends/s3/mock-s3.test.ts @@ -8,7 +8,7 @@ import { jest, spyOn, } from 'bun:test'; -import { createS3Fs } from './s3'; +import { S3Fs } from './s3fs'; describe('s3Fs with mocked client', () => { let mockSend: jest.Mock; @@ -28,7 +28,7 @@ describe('s3Fs with mocked client', () => { }); it('should write a text file', async () => { - const s3fs = createS3Fs('s3://test-bucket/prefix'); + const s3fs = new S3Fs('s3://test-bucket/prefix'); mockSend.mockResolvedValueOnce({}); await s3fs.promises.writeFile('test.txt', 'Hello World', 'utf8'); @@ -46,7 +46,7 @@ describe('s3Fs with mocked client', () => { }); it('should read a text file', async () => { - const s3fs = createS3Fs('s3://test-bucket/prefix'); + const s3fs = new S3Fs('s3://test-bucket/prefix'); const mockBody = { transformToString: jest.fn().mockResolvedValue('Hello World'), transformToByteArray: jest @@ -64,7 +64,7 @@ describe('s3Fs with mocked client', () => { }); it('should handle paths correctly', async () => { - const s3fs = createS3Fs('s3://test-bucket/base/path'); + const s3fs = new S3Fs('s3://test-bucket/base/path'); mockSend.mockResolvedValueOnce({}); await s3fs.promises.writeFile('/some/nested/file.txt', 'content'); @@ -83,7 +83,7 @@ describe('s3Fs with mocked client', () => { }); it('should handle errors', async () => { - const s3fs = createS3Fs('s3://test-bucket'); + const s3fs = new S3Fs('s3://test-bucket'); mockSend.mockRejectedValueOnce(new Error('S3 error')); expect(s3fs.promises.readFile('non-existent.txt')).rejects.toThrow( diff --git a/src/backends/s3/parse-s3-uri.ts b/src/backends/s3/parse-s3-uri.ts index d7ec3bf..099c999 100644 --- a/src/backends/s3/parse-s3-uri.ts +++ b/src/backends/s3/parse-s3-uri.ts @@ -2,6 +2,7 @@ export interface ParsedS3Uri { region?: string; bucket: string; key: string; + forcePathStyle?: boolean; credentials?: { accessKeyId: string; secretAccessKey?: string; @@ -19,10 +20,11 @@ export function parseS3Uri(uri: string): ParsedS3Uri { const [baseUri, queryString] = uri.split('?'); // Parse query params if present - const endpoint = queryString - ?.split('&') - .map((param) => param.split('=')) - .find(([key]) => key === 'endpoint')?.[1]; + const params = new URLSearchParams(queryString); + const endpoint = params.get('endpoint') ?? undefined; + const forcePathStyle = params.has('forcePathStyle') + ? !['no', 'false'].includes(params.get('forcePathStyle') ?? '') + : undefined; if (!baseUri.startsWith('s3://')) { throw new Error('Invalid S3 URI format'); @@ -53,7 +55,8 @@ export function parseS3Uri(uri: string): ParsedS3Uri { region, bucket, key: keyParts.join('/') || '', - ...(endpoint && { endpoint: decodeURIComponent(endpoint) }), + endpoint, + forcePathStyle, }; } @@ -68,7 +71,8 @@ export function parseS3Uri(uri: string): ParsedS3Uri { region, bucket, key: keyParts.join('/') || '', - ...(endpoint && { endpoint: decodeURIComponent(endpoint) }), + endpoint, + forcePathStyle, }; } @@ -82,7 +86,8 @@ export function parseS3Uri(uri: string): ParsedS3Uri { return { bucket, key: keyParts.join('/') || '', - ...(endpoint && { endpoint: decodeURIComponent(endpoint) }), + endpoint, + forcePathStyle, }; } } diff --git a/src/backends/s3/s3.ts b/src/backends/s3/s3.ts deleted file mode 100644 index d030646..0000000 --- a/src/backends/s3/s3.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; -import { parseS3Uri } from './parse-s3-uri'; -import { createS3Client } from './create-s3-client'; - -type ReadFileType = typeof import('fs').promises.readFile; -type WriteFileType = typeof import('fs').promises.writeFile; - -type ArgType< - F extends (...args: any) => any, - N extends number, -> = Parameters[N]; - -type s3Fs = { - promises: { - readFile: ReadFileType; - writeFile: WriteFileType; - }; -}; - -export const createS3Fs = (bucketUri: string): s3Fs => { - const client = createS3Client(bucketUri); - const parsed = parseS3Uri(bucketUri); - const basePath = parsed.key; - - const normalizePath = (path: string): { bucket: string; key: string } => { - // Remove leading slash if present - const normalizedPath = path.startsWith('/') ? path.slice(1) : path; - - // Combine base path with provided path, avoiding double slashes - const fullPath = basePath - ? `${basePath.replace(/\/$/, '')}/${normalizedPath}` - : normalizedPath; - - return { - bucket: parsed.bucket, - key: fullPath, - }; - }; - - const readFile = async ( - path: ArgType, - options?: ArgType - ): Promise => { - const pathStr = path.toString(); - const { bucket, key } = normalizePath(pathStr); - - const command = new GetObjectCommand({ - Bucket: bucket, - Key: key, - }); - - try { - const response = await client.send(command); - - if (!response.Body) { - throw new Error('Empty response body'); - } - - // Convert the readable stream to a buffer - const stream = response.Body; - - // Handle encoding if specified - if (typeof options === 'string') { - return stream.transformToString(options); - } else if (options?.encoding) { - return stream.transformToString(options.encoding); - } - - return Buffer.from(await stream.transformToByteArray()); - } catch (error) { - // Transform AWS errors to match fs.readFile error format - const err = error instanceof Error ? error : new Error('Unknown error'); - err.message = `Error reading file from S3: ${err.message}`; - throw err; - } - }; - - const writeFile = async ( - path: ArgType, - data: ArgType, - options?: ArgType - ): Promise => { - const pathStr = path.toString(); - const { bucket, key } = normalizePath(pathStr); - - let buffer: Buffer; - if (typeof data === 'string') { - const encoding = - typeof options === 'string' ? options : options?.encoding; - - buffer = Buffer.from(data, encoding ?? 'utf8'); - } else { - buffer = Buffer.from(data as Uint8Array); - } - - const command = new PutObjectCommand({ - Bucket: bucket, - Key: key, - Body: buffer, - ContentLength: buffer.length, - }); - - try { - await client.send(command); - } catch (error) { - // Transform AWS errors to match fs.writeFile error format - const err = error instanceof Error ? error : new Error('Unknown error'); - err.message = `Error writing file to S3: ${err.message}`; - throw err; - } - }; - - return { - promises: { - // because return type overloads are not the same as the union return type - readFile: readFile as ReadFileType, - writeFile, - }, - }; -}; diff --git a/src/backends/s3/s3fs.ts b/src/backends/s3/s3fs.ts new file mode 100644 index 0000000..c81da63 --- /dev/null +++ b/src/backends/s3/s3fs.ts @@ -0,0 +1,138 @@ +import { + GetObjectCommand, + PutObjectCommand, + S3Client, +} from '@aws-sdk/client-s3'; +import stream from 'node:stream'; +import { createS3Client } from './create-s3-client'; +import { ParsedS3Uri, parseS3Uri } from './parse-s3-uri'; +import { createWriteStream } from './create-write-stream'; + +class S3BaseFs { + protected client: S3Client; + private parsed: ParsedS3Uri; + constructor(protected bucketUri: `s3://${string}`) { + const client = createS3Client(bucketUri); + this.parsed = parseS3Uri(bucketUri); + this.client = client; + } + protected normalizePath(path: string): { bucket: string; key: string } { + const basePath = this.parsed.key; + + // Remove leading slash if present + const normalizedPath = path.startsWith('/') ? path.slice(1) : path; + + // Combine base path with provided path, avoiding double slashes + const fullPath = basePath + ? `${basePath.replace(/\/$/, '')}/${normalizedPath}` + : normalizedPath; + + return { + bucket: this.parsed.bucket, + key: fullPath, + }; + } +} + +export class S3PromiseFs extends S3BaseFs { + public async writeFile( + path: string, + data: + | string + | NodeJS.ArrayBufferView + | Iterable + | AsyncIterable + | stream.Stream, + options?: + | BufferEncoding + | { + encoding?: BufferEncoding; + } + ): Promise { + const { bucket, key } = this.normalizePath(path); + + let buffer: Buffer; + if (typeof data === 'string') { + const encoding = + typeof options === 'string' ? options : options?.encoding; + + buffer = Buffer.from(data, encoding ?? 'utf8'); + } else { + buffer = Buffer.from(data as Uint8Array); + } + + const command = new PutObjectCommand({ + Bucket: bucket, + Key: key, + Body: buffer, + ContentLength: buffer.length, + }); + + try { + await this.client.send(command); + } catch (error) { + // Transform AWS errors to match fs.writeFile error format + const err = error instanceof Error ? error : new Error('Unknown error'); + err.message = `Error writing file to S3: ${err.message}`; + throw err; + } + } + + public async readFile(path: string): Promise; + public async readFile( + path: string, + options: { + encoding: BufferEncoding; + } + ): Promise; + public async readFile(path: string, options: BufferEncoding): Promise; + public async readFile( + path: string, + options?: + | BufferEncoding + | { + encoding?: BufferEncoding; + } + ): Promise { + const pathStr = path.toString(); + const { bucket, key } = this.normalizePath(pathStr); + + const command = new GetObjectCommand({ + Bucket: bucket, + Key: key, + }); + + try { + const response = await this.client.send(command); + + if (!response.Body) { + throw new Error('Empty response body'); + } + + // Convert the readable stream to a buffer + const stream = response.Body; + + // Handle encoding if specified + if (typeof options === 'string') { + return stream.transformToString(options); + } else if (options && typeof options?.encoding === 'string') { + return stream.transformToString(options.encoding); + } + + return Buffer.from(await stream.transformToByteArray()); + } catch (error) { + // Transform AWS errors to match fs.readFile error format + const err = error instanceof Error ? error : new Error('Unknown error'); + err.message = `Error reading file from S3: ${err.message}`; + throw err; + } + } +} + +export class S3Fs extends S3BaseFs { + public promises = new S3PromiseFs(this.bucketUri); + public createWriteStream(path: string) { + const { bucket, key } = this.normalizePath(path); + return createWriteStream(this.client, bucket, key); + } +} diff --git a/src/index.ts b/src/index.ts index 7e6bc8c..9bc2188 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,4 @@ export { nafs } from './nafs'; +export { S3Fs, S3PromiseFs } from './backends/s3/s3fs'; +export { createMemFs } from './backends/local/memory'; +export { createFileFs } from './backends/local/file'; diff --git a/src/nafs.ts b/src/nafs.ts index 4e3d476..e2cbb20 100644 --- a/src/nafs.ts +++ b/src/nafs.ts @@ -1,51 +1,45 @@ -import type { IFs } from 'memfs'; +import { parseUri, proto } from './parse-uri'; -type NAFSOptions = { - /** - * only used when the storage is a file:// - */ - fs?: IFs; - /** - * only used when the storage is a s3:// - */ - s3?: { +export async function nafs( + uri: `s3://${string}`, + options?: { accessKeyId: string; secretAccessKey: string; region: string; - }; -}; - -export const nafs = async ( - storage: string, - options?: NAFSOptions -): Promise => { - if (storage === ':memory:') { + } +): Promise; +/** + * @returns A [linkfs](https://github.com/ricsam/linkfs) instance + */ +export async function nafs( + uri: `file://${string}` +): Promise; +/** + * @returns A [memfs](https://github.com/streamich/memfs) instance + */ +export async function nafs(uri: ':memory:'): Promise; +export async function nafs( + uri: `${proto}://${string}` | ':memory:' +): Promise { + if (uri === ':memory:') { const { createMemFs } = await import('./backends/local/memory'); return createMemFs(); } - let proto: string; - let uri: string; - const r = storage.match(/^([^:]+):\/\/(.+)/); - if (r) { - proto = r[1]; - uri = r[2]; - } else { - throw new Error('Invalid url'); - } + const parsed = parseUri(uri); - switch (proto) { + switch (parsed.proto) { case 'file': { const { createFileFs } = await import('./backends/local/file'); - const lfs = createFileFs(uri, options?.fs); + const lfs = createFileFs(parsed.uri); return lfs as any; } case 's3': { - const { createS3Fs } = await import('./backends/s3/s3'); - return createS3Fs(uri) as any; + const { S3Fs } = await import('./backends/s3/s3fs'); + return new S3Fs(parsed.uri) as any; } default: { throw new Error('Invalid url supplied, the protocol is not supported'); } } -}; +} diff --git a/src/parse-uri.ts b/src/parse-uri.ts new file mode 100644 index 0000000..198e2ed --- /dev/null +++ b/src/parse-uri.ts @@ -0,0 +1,31 @@ +export type proto = 'file' | 's3' | 'memory'; + +export type ParsedUri = + | { + proto: 'file'; + path: string; + uri: `file://${string}`; + } + | { + proto: 's3'; + path: string; + uri: `s3://${string}`; + } + | { + proto: 'memory'; + path: ':memory:'; + uri: ':memory:'; + }; + +export function parseUri(uri: `${proto}://${string}` | ':memory:'): ParsedUri { + let proto: string; + let path: string; + const r = uri.match(/^([^:]+):\/\/(.+)/); + if (r) { + proto = r[1]; + path = r[2]; + } else { + throw new Error('Invalid URI'); + } + return { proto, path, uri } as any; +}