r2-explorer 0.6.0 → 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Gabriel Massadas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,89 +1,41 @@
1
1
  # R2-Explorer
2
2
 
3
- A Google Drive Interface for your Cloudflare R2 Buckets!
3
+ <p align="center">
4
+ <em>A Google Drive Interface for your Cloudflare R2 Buckets!</em>
5
+ </p>
4
6
 
5
- This project is deployed/self-hosted in your own Cloudflare Account as a Worker, and no credential/token is required to
6
- start using it.
7
+ <p>
8
+ This project is deployed/self-hosted in your own Cloudflare Account as a Worker, and no credential/token is required to
9
+ start using it.
10
+ </p>
7
11
 
8
- You can see an live example, in `read-only` mode, in your browser at https://r2-explorer.massadas.com/
12
+ ---
9
13
 
10
- This project is still in development, and there are definitely going to be some weird issues sometimes, but when you
11
- find something
12
- please [open an new issue](https://github.com/G4brym/R2-Explorer/issues/new) for it to get solved.
14
+ **Documentation**: <a href="https://r2explorer.dev" target="_blank">https://r2explorer.dev</a>
15
+
16
+ **Live Demo**: <a href="https://demo.r2explorer.dev" target="_blank">https://demo.r2explorer.dev</a>
17
+
18
+ ---
13
19
 
14
20
  ## Features
15
21
 
22
+ - [Email Explorer](https://r2explorer.dev/guides/setup-email-explorer/) (using Cloudflare Email Routing)
23
+ - [Cloudflare Access Authentication](https://r2explorer.dev/getting-started/security/)
16
24
  - Very quick bucket/folder navigation
17
25
  - pdf, image, txt, markdown, csv, etc in-browser preview
18
26
  - Drag-and-Drop upload
19
27
  - Multiple files and folder uploads
20
28
  - Create folders
21
- - Rename files
22
- - Download files
23
- - Delete files
29
+ - Upload/Rename/Download/Delete files
24
30
  - Right click in file for extra options
25
31
  - Multipart upload for big files
26
- - Cloudflare Access validation using jwt
27
-
28
- ## Configurations
29
-
30
- These options are defined in the `index.js` file, in the `R2Explorer({ readonly: false, ... })`.
31
-
32
- | Name | Type(s) | Description | Examples |
33
- |--------------------|--------------------------|-----------------------------------------------------------------------|-----------------------------------------------------------|
34
- | `readonly` | `boolean` or `undefined` | Controls the write access globally, default: `true` | `true` |
35
- | `cors` | `boolean` or `undefined` | Enables or disables CORS access to the internal API, default: `false` | `true` |
36
- | `cfAccessTeamName` | `string` or `undefined` | When set enforces Cloudflare Access in all requests | `radar` (taken from https://radar.cloudflareaccess.com/) |
37
-
38
-
39
- ## FAQ
40
-
41
- Q. Is there any Authentication for r2-explorer?
42
-
43
- A. No. If you want authenticated access, you must
44
- setup [Cloudflare Access](https://www.cloudflare.com/products/zero-trust/access/) in your account.
45
- Access is free up to 50 users.
46
-
47
- ___
48
-
49
- Q. Can i upload files bigger than 100MB?
50
-
51
- A. Yes! R2-Explorer now
52
- support's [Multipart Upload](https://developers.cloudflare.com/r2/data-access/workers-api/workers-multipart-usage/),
53
- that splits the files you are uploading in about 95MB chunks for uploading within the Cloudflare 100MB uploading limit.
54
32
 
55
33
  ## Getting Started
56
34
 
57
35
  Run this command to get an example project setup
58
36
 
59
37
  ```bash
60
- npx r2-explorer my-r2-explorer
61
- ```
62
-
63
- Change into the newly created directory and install the packages
64
-
65
- ```bash
66
- cd my-r2-explorer
67
- npm install
68
- ```
69
-
70
- Update the `wrangler.toml` with your R2 Buckets (tip: you can setup as many Buckets as your want)
71
-
72
- ```
73
- - wrangler.toml -
74
- ...
75
- [[r2_buckets]]
76
- binding = 'my-bucket-name'
77
- bucket_name = 'my-bucket-name'
78
- preview_bucket_name = 'my-bucket-name'
79
- ```
80
-
81
- If you want to be able to upload/modify your buckets, you must update the `readonly` flag in `src/index.ts` file.
82
-
83
- After that just run publish and the project will be up and running for you and everyone you invite to use the Buckets
84
-
85
- ```bash
86
- wrangler publish
38
+ npm create r2-explorer@latest
87
39
  ```
88
40
 
89
41
  ## Upgrading your installation
@@ -108,46 +60,9 @@ wrangler publish
108
60
  - Image thumbnail's using Cloudflare workers
109
61
  - Tooltip when hovering a file with absolute time in "x days time ago" format
110
62
  - Automatically load more files, when the bottom is reached (current limit is 1000 files)
111
- - Download files bigger than 2gb with presigned url's
112
- - set file navigation in the url to allow direct share of a specific file
113
- - only support previews to files under 100mb
63
+ - bundle bootstrap icons instead of importing
114
64
 
115
65
  ## Known issues
116
66
 
117
67
  - Rename files with special characters is not possible with
118
68
  current [sdk issue here](https://github.com/aws/aws-sdk-js/issues/1949)
119
-
120
- ## Images
121
-
122
- Home Page
123
- ![Home](https://github.com/G4brym/R2-Explorer/raw/main/docs/images/home.png)
124
-
125
- Image Previewer
126
- ![Home](https://github.com/G4brym/R2-Explorer/raw/main/docs/images/image-preview.png)
127
-
128
- Pdf Previewer
129
- ![Home](https://github.com/G4brym/R2-Explorer/raw/main/docs/images/pdf-preview.png)
130
-
131
- New Folder
132
- ![Home](https://github.com/G4brym/R2-Explorer/raw/main/docs/images/new-folder.png)
133
-
134
- Uploading Files
135
- ![Home](https://github.com/G4brym/R2-Explorer/raw/main/docs/images/uploading-files.png)
136
-
137
- ### Compiles and hot-reloads for development
138
-
139
- ```
140
- npm run serve
141
- ```
142
-
143
- ### Compiles and minifies for production
144
-
145
- ```
146
- npm run build
147
- ```
148
-
149
- ### Lints and fixes files
150
-
151
- ```
152
- npm run lint
153
- ```
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("itty-router"),t=require("@cloudflare/itty-router-openapi"),a=require("zod");const s=!0,o=!1,n="1.0.1";class r extends t.OpenAPIRoute{static schema={operationId:"get-bucket-list",tags:["Buckets"],summary:"List buckets"};async handle(e,t,a,s){const o=[];for(const[e,a]of Object.entries(t))a.get&&a.put&&o.push({name:e});return{buckets:o}}}class c extends t.OpenAPIRoute{static schema={operationId:"get-bucket-list-objects",tags:["Buckets"],summary:"List objects",parameters:{bucket:t.Path(String),limit:t.Query(a.z.number().optional()),prefix:t.Query(a.z.string().optional().describe("base64 encoded prefix")),cursor:t.Query(a.z.string().optional()),delimiter:t.Query(a.z.string().optional()),include:t.Query(a.z.string().array().optional())}};async handle(e,t,a,s){const o=t[s.params.bucket];return await o.list({limit:s.query.limit,prefix:s.query.prefix?decodeURIComponent(escape(atob(s.query.prefix))):void 0,cursor:s.query.cursor,delimiter:s.query.delimiter,include:s.query.include})}}class i extends t.OpenAPIRoute{static schema={operationId:"post-bucket-move-object",tags:["Buckets"],summary:"Move object",parameters:{bucket:t.Path(String)},requestBody:{oldKey:a.z.string().optional().describe("base64 encoded file key"),newKey:a.z.string().optional().describe("base64 encoded file key")}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.body.oldKey))),r=decodeURIComponent(escape(atob(s.body.newKey))),c=await o.get(n),i=await o.put(r,c.body,{customMetadata:c.customMetadata,httpMetadata:c.httpMetadata});return await o.delete(n),i}}class u extends t.OpenAPIRoute{static schema={operationId:"post-bucket-create-folder",tags:["Buckets"],summary:"Create folder",parameters:{bucket:t.Path(String)},requestBody:{key:a.z.string().optional().describe("base64 encoded file key")}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.body.key)));return await o.put(n,"R2 Explorer Folder")}}class d extends t.OpenAPIRoute{static schema={operationId:"post-bucket-upload-object",tags:["Buckets"],summary:"Upload object",parameters:{bucket:t.Path(String),key:t.Query(a.z.string().optional().describe("base64 encoded file key")),customMetadata:t.Query(a.z.string().optional().describe("base64 encoded json string")),httpMetadata:t.Query(a.z.string().optional().describe("base64 encoded json string"))}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket];let n,r,c=decodeURIComponent(escape(atob(s.query.key)));return s.query.customMetadata&&(n=JSON.parse(decodeURIComponent(escape(atob(s.query.customMetadata))))),s.query.httpMetadata&&(r=JSON.parse(decodeURIComponent(escape(atob(s.query.httpMetadata))))),await o.put(c,e.body,{customMetadata:n,httpMetadata:r})}}class p extends t.OpenAPIRoute{static schema={operationId:"post-bucket-delete-object",tags:["Buckets"],summary:"Delete object",parameters:{bucket:t.Path(String)},requestBody:{key:a.z.string().optional().describe("base64 encoded file key")}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.body.key)));return await o.delete(n)}}class l extends t.OpenAPIRoute{static schema={operationId:"post-multipart-create-upload",tags:["Multipart"],summary:"Create upload",parameters:{bucket:t.Path(String),key:t.Query(a.z.string().optional().describe("base64 encoded file key")),customMetadata:t.Query(a.z.string().optional().describe("base64 encoded json string")),httpMetadata:t.Query(a.z.string().optional().describe("base64 encoded json string"))}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.query.key)));let r,c;return s.query.customMetadata&&(r=JSON.parse(decodeURIComponent(escape(atob(s.query.customMetadata))))),s.query.httpMetadata&&(c=JSON.parse(decodeURIComponent(escape(atob(s.query.httpMetadata))))),await o.createMultipartUpload(n,{customMetadata:r,httpMetadata:c})}}class m extends t.OpenAPIRoute{static schema={operationId:"post-multipart-part-upload",tags:["Multipart"],summary:"Part upload",parameters:{bucket:t.Path(String),key:t.Query(a.z.string().optional().describe("base64 encoded file key")),uploadId:t.Query(String),partNumber:t.Query(t.Int)}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.query.key))),r=o.resumeMultipartUpload(n,s.query.uploadId);try{return await r.uploadPart(s.query.partNumber,e.body)}catch(e){return new Response(e.message,{status:400})}}}class y extends t.OpenAPIRoute{static schema={operationId:"post-multipart-complete-upload",tags:["Multipart"],summary:"Complete upload",parameters:{bucket:t.Path(String)},requestBody:{uploadId:String,key:a.z.string().optional().describe("base64 encoded file key"),parts:[{etag:String,partNumber:t.Int}]}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=s.body.uploadId,r=decodeURIComponent(escape(atob(s.body.key))),c=s.body.parts,i=await o.resumeMultipartUpload(r,n);try{return{success:!0,str:await i.complete(c)}}catch(e){return Response.json({msg:e.message},{status:400})}}}class b extends t.OpenAPIRoute{static schema={operationId:"get-bucket-object",tags:["Buckets"],summary:"Get Object",parameters:{bucket:t.Path(String),key:t.Path(a.z.string().optional().describe("base64 encoded file key"))},responses:{200:{description:"File binary",schema:a.z.string().openapi({format:"binary"})}}};async handle(e,t,a,s){const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.params.key))),r=await o.get(n);if(null===r)return Response.json({msg:"Object Not Found"},{status:404});const c=new Headers;return r.writeHttpMetadata(c),c.set("etag",r.httpEtag),c.set("Content-Disposition",`attachment; filename="${n.split("/").pop()}"`),new Response(r.body,{headers:c})}}class g extends t.OpenAPIRoute{static schema={operationId:"Head-bucket-object",tags:["Buckets"],summary:"Get Object",parameters:{bucket:t.Path(String),key:t.Query(a.z.string().optional().describe("base64 encoded file key"))}};async handle(e,t,a,s){const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.params.key))),r=await o.head(n);return null===r?Response.json({msg:"Object Not Found"},{status:404}):r}}class f extends t.OpenAPIRoute{static schema={operationId:"post-bucket-put-object-metadata",tags:["Buckets"],summary:"Update object metadata",parameters:{bucket:t.Path(String),key:t.Path(a.z.string().describe("base64 encoded file key"))},requestBody:{customMetadata:a.z.record(a.z.string(),a.z.any())}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.params.key))),r=await o.get(n);return await o.put(n,r.body,{customMetadata:s.body.customMetadata})}}const h=t.OpenAPIRouter({base:"/api/buckets",raiseUnknownParameters:s,generateOperationIds:o});h.get("",r),h.get("/:bucket",c),h.post("/:bucket/move",i),h.post("/:bucket/folder",u),h.post("/:bucket/upload",d),h.post("/:bucket/multipart/create",l),h.post("/:bucket/multipart/upload",m),h.post("/:bucket/multipart/complete",y),h.post("/:bucket/delete",p),h.head("/:bucket/:key",g),h.get("/:bucket/:key",b),h.post("/:bucket/:key",f);let k={},w=0;function R(e){return`https://${e}.cloudflareaccess.com`}async function I(e,t,a){let s=!1;try{s=await async function(e,t){const a=function(e){const t=e.headers.get("cf-access-jwt-assertion");if(!t)return null;return t.trim()}(e);if(null===a)return!1;(0===Object.keys(k).length||Math.floor(Date.now()/1e3)<w)&&(k=await async function(e){let t=`${R(e.cfAccessTeamName)}/cdn-cgi/access/certs`;const a=await fetch(t,{method:"GET",cf:{cacheTtlByStatus:{"200-299":30,"300-599":0}},headers:{"Content-Type":"application/json; charset=UTF-8"}}),s=await a.json();w=Math.floor(Date.now()/1e3)+3600;const o={};for(const e of s.keys)o[e.kid]=await crypto.subtle.importKey("jwk",e,{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"},!1,["verify"]);return o}(t));let s;try{s=function(e){const t=e.split("."),a=JSON.parse(atob(t[0])),s=JSON.parse(atob(t[1])),o=atob(t[2].replace(/_/g,"/").replace(/-/g,"+"));return{header:a,payload:s,signature:o,raw:{header:t[0],payload:t[1],signature:t[2]}}}(a)}catch(e){return!1}const o=new Date(1e3*s.payload.exp),n=new Date(Date.now());if(o<=n)return console.log("expired token"),!1;if(s.payload?.iss!==R(t.cfAccessTeamName))return console.log("invalid access signer"),!1;if(!async function(e){const t=(new TextEncoder).encode([e.raw.header,e.raw.payload].join(".")),a=new Uint8Array(Array.from(e.signature).map((e=>e.charCodeAt(0))));for(const e of Object.values(k)){if(await j(e,a,t))return!0}return!1}(s))return!1;return s}(e,a.config)}catch(e){}if(!1===s)return Response.json({success:!1,errors:[{code:1e4,message:"Authentication error! Verify that the Cloudflare Access Team name is correct"}]},{status:401});a.username=s.payload.email}async function j(e,t,a){return crypto.subtle.verify("RSASSA-PKCS1-v1_5",e,t,a)}async function x(e,t,a){const s=caches.default;let o,n=new URL(e.url).pathname;if(n.includes(".")||(n="/"),!1!==a.config.cacheAssets&&(o=await s.match(e),o))return o;let r="https://demo.r2explorer.dev";a.config?.dashboardUrl&&(r=a.config.dashboardUrl.endsWith("/")?a.config.dashboardUrl.slice(0,-1):a.config.dashboardUrl);const c=await fetch(`${r}${n}`);return o=new Response(await c.text(),{status:c.status,headers:{"Content-Type":c.headers.get("Content-Type"),"Access-Control-Allow-Origin":"*","Cache-Control":"max-age: 300"}}),200===c.status&&!1!==a.config.cacheAssets&&a.executionContext.waitUntil(s.put(e,o.clone())),o}const M=require("postal-mime/dist/node").postalMime.default;async function P(e,t,a){let s;if(a.config?.emailRouting?.targetBucket&&t[a.config.emailRouting.targetBucket]&&(s=t[a.config.emailRouting.targetBucket]),!s)for(const[e,a]of Object.entries(t))if(a.get&&a.put){s=a;break}const o=await async function(e,t){let a=new Uint8Array(t),s=0;const o=e.getReader();for(;;){const{done:e,value:t}=await o.read();if(e)break;a.set(t,s),s+=t.length}return a}(e.raw,e.rawSize),n=new M,r=await n.parse(o),c=`${Math.floor(Date.now())}-${crypto.randomUUID()}`;await s.put(`.r2-explorer/emails/inbox/${c}.json`,JSON.stringify(r),{customMetadata:{subject:r.subject,from_address:r.from?.address,from_name:r.from?.name,to_address:r.to.length>0?r.to[0].address:null,to_name:r.to.length>0?r.to[0].name:null,has_attachments:r.attachments.length>0,read:!1}});for(const e of r.attachments)await s.put(`.r2-explorer/emails/inbox/${c}/${e.filename}`,e.content)}class A extends t.OpenAPIRoute{static schema={operationId:"get-server-info",tags:["Server"],summary:"Get server info"};async handle(e,t,a,s){return{version:n,config:a.config,user:{username:a.username}}}}const O=t.OpenAPIRouter({base:"/api/server",raiseUnknownParameters:s,generateOperationIds:o});O.get("/config",A),exports.R2Explorer=function(a){!1!==(a=a||{}).readonly&&(a.readonly=!0);const s=t.OpenAPIRouter({schema:{info:{title:"R2 Explorer API",version:n}}}),{preflight:o,corsify:r}=e.createCors();return a.cfAccessTeamName&&s.all("*",I),!0===a.cors&&s.all("*",o),s.all("/api/server/*",O),s.all("/api/buckets/*",h),s.original.get("*",x),s.all("*",(()=>Response.json({msg:"404, not found!"},{status:404}))),{async email(e,t,s){await P(e,t,{executionContext:s,config:a})},async fetch(e,t,o){let n=await s.handle(e,t,{executionContext:o,config:a});return!0===a.cors&&(n=r(n)),n}}};
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ import{createCors as e}from"itty-router";import{OpenAPIRoute as t,Path as a,Query as s,Int as o,OpenAPIRouter as n}from"@cloudflare/itty-router-openapi";import{z as r}from"zod";const c=!0,i=!1,d="1.0.1";class u extends t{static schema={operationId:"get-bucket-list-objects",tags:["Buckets"],summary:"List objects",parameters:{bucket:a(String),limit:s(r.number().optional()),prefix:s(r.string().optional().describe("base64 encoded prefix")),cursor:s(r.string().optional()),delimiter:s(r.string().optional()),include:s(r.string().array().optional())}};async handle(e,t,a,s){const o=t[s.params.bucket];return await o.list({limit:s.query.limit,prefix:s.query.prefix?decodeURIComponent(escape(atob(s.query.prefix))):void 0,cursor:s.query.cursor,delimiter:s.query.delimiter,include:s.query.include})}}class p extends t{static schema={operationId:"post-bucket-move-object",tags:["Buckets"],summary:"Move object",parameters:{bucket:a(String)},requestBody:{oldKey:r.string().optional().describe("base64 encoded file key"),newKey:r.string().optional().describe("base64 encoded file key")}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.body.oldKey))),r=decodeURIComponent(escape(atob(s.body.newKey))),c=await o.get(n),i=await o.put(r,c.body,{customMetadata:c.customMetadata,httpMetadata:c.httpMetadata});return await o.delete(n),i}}class l extends t{static schema={operationId:"post-bucket-create-folder",tags:["Buckets"],summary:"Create folder",parameters:{bucket:a(String)},requestBody:{key:r.string().optional().describe("base64 encoded file key")}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.body.key)));return await o.put(n,"R2 Explorer Folder")}}class m extends t{static schema={operationId:"post-bucket-upload-object",tags:["Buckets"],summary:"Upload object",parameters:{bucket:a(String),key:s(r.string().optional().describe("base64 encoded file key")),customMetadata:s(r.string().optional().describe("base64 encoded json string")),httpMetadata:s(r.string().optional().describe("base64 encoded json string"))}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket];let n,r,c=decodeURIComponent(escape(atob(s.query.key)));return s.query.customMetadata&&(n=JSON.parse(decodeURIComponent(escape(atob(s.query.customMetadata))))),s.query.httpMetadata&&(r=JSON.parse(decodeURIComponent(escape(atob(s.query.httpMetadata))))),await o.put(c,e.body,{customMetadata:n,httpMetadata:r})}}class y extends t{static schema={operationId:"post-bucket-delete-object",tags:["Buckets"],summary:"Delete object",parameters:{bucket:a(String)},requestBody:{key:r.string().optional().describe("base64 encoded file key")}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.body.key)));return await o.delete(n)}}class b extends t{static schema={operationId:"post-multipart-create-upload",tags:["Multipart"],summary:"Create upload",parameters:{bucket:a(String),key:s(r.string().optional().describe("base64 encoded file key")),customMetadata:s(r.string().optional().describe("base64 encoded json string")),httpMetadata:s(r.string().optional().describe("base64 encoded json string"))}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.query.key)));let r,c;return s.query.customMetadata&&(r=JSON.parse(decodeURIComponent(escape(atob(s.query.customMetadata))))),s.query.httpMetadata&&(c=JSON.parse(decodeURIComponent(escape(atob(s.query.httpMetadata))))),await o.createMultipartUpload(n,{customMetadata:r,httpMetadata:c})}}class g extends t{static schema={operationId:"post-multipart-part-upload",tags:["Multipart"],summary:"Part upload",parameters:{bucket:a(String),key:s(r.string().optional().describe("base64 encoded file key")),uploadId:s(String),partNumber:s(o)}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.query.key))),r=o.resumeMultipartUpload(n,s.query.uploadId);try{return await r.uploadPart(s.query.partNumber,e.body)}catch(e){return new Response(e.message,{status:400})}}}class f extends t{static schema={operationId:"post-multipart-complete-upload",tags:["Multipart"],summary:"Complete upload",parameters:{bucket:a(String)},requestBody:{uploadId:String,key:r.string().optional().describe("base64 encoded file key"),parts:[{etag:String,partNumber:o}]}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=s.body.uploadId,r=decodeURIComponent(escape(atob(s.body.key))),c=s.body.parts,i=await o.resumeMultipartUpload(r,n);try{return{success:!0,str:await i.complete(c)}}catch(e){return Response.json({msg:e.message},{status:400})}}}class h extends t{static schema={operationId:"get-bucket-object",tags:["Buckets"],summary:"Get Object",parameters:{bucket:a(String),key:a(r.string().optional().describe("base64 encoded file key"))},responses:{200:{description:"File binary",schema:r.string().openapi({format:"binary"})}}};async handle(e,t,a,s){const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.params.key))),r=await o.get(n);if(null===r)return Response.json({msg:"Object Not Found"},{status:404});const c=new Headers;return r.writeHttpMetadata(c),c.set("etag",r.httpEtag),c.set("Content-Disposition",`attachment; filename="${n.split("/").pop()}"`),new Response(r.body,{headers:c})}}class k extends t{static schema={operationId:"Head-bucket-object",tags:["Buckets"],summary:"Get Object",parameters:{bucket:a(String),key:s(r.string().optional().describe("base64 encoded file key"))}};async handle(e,t,a,s){const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.params.key))),r=await o.head(n);return null===r?Response.json({msg:"Object Not Found"},{status:404}):r}}class w extends t{static schema={operationId:"post-bucket-put-object-metadata",tags:["Buckets"],summary:"Update object metadata",parameters:{bucket:a(String),key:a(r.string().describe("base64 encoded file key"))},requestBody:{customMetadata:r.record(r.string(),r.any())}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],n=decodeURIComponent(escape(atob(s.params.key))),r=await o.get(n);return await o.put(n,r.body,{customMetadata:s.body.customMetadata})}}const j=n({base:"/api/buckets",raiseUnknownParameters:c,generateOperationIds:i});j.get("",class extends t{static schema={operationId:"get-bucket-list",tags:["Buckets"],summary:"List buckets"};async handle(e,t,a,s){const o=[];for(const[e,a]of Object.entries(t))a.get&&a.put&&o.push({name:e});return{buckets:o}}}),j.get("/:bucket",u),j.post("/:bucket/move",p),j.post("/:bucket/folder",l),j.post("/:bucket/upload",m),j.post("/:bucket/multipart/create",b),j.post("/:bucket/multipart/upload",g),j.post("/:bucket/multipart/complete",f),j.post("/:bucket/delete",y),j.head("/:bucket/:key",k),j.get("/:bucket/:key",h),j.post("/:bucket/:key",w);let R={},x=0;function I(e){return`https://${e}.cloudflareaccess.com`}async function M(e,t,a){let s=!1;try{s=await async function(e,t){const a=function(e){const t=e.headers.get("cf-access-jwt-assertion");if(!t)return null;return t.trim()}(e);if(null===a)return!1;(0===Object.keys(R).length||Math.floor(Date.now()/1e3)<x)&&(R=await async function(e){let t=`${I(e.cfAccessTeamName)}/cdn-cgi/access/certs`;const a=await fetch(t,{method:"GET",cf:{cacheTtlByStatus:{"200-299":30,"300-599":0}},headers:{"Content-Type":"application/json; charset=UTF-8"}}),s=await a.json();x=Math.floor(Date.now()/1e3)+3600;const o={};for(const e of s.keys)o[e.kid]=await crypto.subtle.importKey("jwk",e,{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"},!1,["verify"]);return o}(t));let s;try{s=function(e){const t=e.split("."),a=JSON.parse(atob(t[0])),s=JSON.parse(atob(t[1])),o=atob(t[2].replace(/_/g,"/").replace(/-/g,"+"));return{header:a,payload:s,signature:o,raw:{header:t[0],payload:t[1],signature:t[2]}}}(a)}catch(e){return!1}const o=new Date(1e3*s.payload.exp),n=new Date(Date.now());if(o<=n)return console.log("expired token"),!1;if(s.payload?.iss!==I(t.cfAccessTeamName))return console.log("invalid access signer"),!1;if(!async function(e){const t=(new TextEncoder).encode([e.raw.header,e.raw.payload].join(".")),a=new Uint8Array(Array.from(e.signature).map((e=>e.charCodeAt(0))));for(const e of Object.values(R)){if(await U(e,a,t))return!0}return!1}(s))return!1;return s}(e,a.config)}catch(e){}if(!1===s)return Response.json({success:!1,errors:[{code:1e4,message:"Authentication error! Verify that the Cloudflare Access Team name is correct"}]},{status:401});a.username=s.payload.email}async function U(e,t,a){return crypto.subtle.verify("RSASSA-PKCS1-v1_5",e,t,a)}async function C(e,t,a){const s=caches.default;let o,n=new URL(e.url).pathname;if(n.includes(".")||(n="/"),!1!==a.config.cacheAssets&&(o=await s.match(e),o))return o;let r="https://demo.r2explorer.dev";a.config?.dashboardUrl&&(r=a.config.dashboardUrl.endsWith("/")?a.config.dashboardUrl.slice(0,-1):a.config.dashboardUrl);const c=await fetch(`${r}${n}`);return o=new Response(await c.text(),{status:c.status,headers:{"Content-Type":c.headers.get("Content-Type"),"Access-Control-Allow-Origin":"*","Cache-Control":"max-age: 300"}}),200===c.status&&!1!==a.config.cacheAssets&&a.executionContext.waitUntil(s.put(e,o.clone())),o}const S=require("postal-mime/dist/node").postalMime.default;async function q(e,t,a){let s;if(a.config?.emailRouting?.targetBucket&&t[a.config.emailRouting.targetBucket]&&(s=t[a.config.emailRouting.targetBucket]),!s)for(const[e,a]of Object.entries(t))if(a.get&&a.put){s=a;break}const o=await async function(e,t){let a=new Uint8Array(t),s=0;const o=e.getReader();for(;;){const{done:e,value:t}=await o.read();if(e)break;a.set(t,s),s+=t.length}return a}(e.raw,e.rawSize),n=new S,r=await n.parse(o),c=`${Math.floor(Date.now())}-${crypto.randomUUID()}`;await s.put(`.r2-explorer/emails/inbox/${c}.json`,JSON.stringify(r),{customMetadata:{subject:r.subject,from_address:r.from?.address,from_name:r.from?.name,to_address:r.to.length>0?r.to[0].address:null,to_name:r.to.length>0?r.to[0].name:null,has_attachments:r.attachments.length>0,read:!1}});for(const e of r.attachments)await s.put(`.r2-explorer/emails/inbox/${c}/${e.filename}`,e.content)}const v=n({base:"/api/server",raiseUnknownParameters:c,generateOperationIds:i});function A(t){!1!==(t=t||{}).readonly&&(t.readonly=!0);const a=n({schema:{info:{title:"R2 Explorer API",version:d}}}),{preflight:s,corsify:o}=e();return t.cfAccessTeamName&&a.all("*",M),!0===t.cors&&a.all("*",s),a.all("/api/server/*",v),a.all("/api/buckets/*",j),a.original.get("*",C),a.all("*",(()=>Response.json({msg:"404, not found!"},{status:404}))),{async email(e,a,s){await q(e,a,{executionContext:s,config:t})},async fetch(e,s,n){let r=await a.handle(e,s,{executionContext:n,config:t});return!0===t.cors&&(r=o(r)),r}}}v.get("/config",class extends t{static schema={operationId:"get-server-info",tags:["Server"],summary:"Get server info"};async handle(e,t,a,s){return{version:d,config:a.config,user:{username:a.username}}}});export{A as R2Explorer};
@@ -0,0 +1,4 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { Context, R2ExplorerConfig } from "../../interfaces";
3
+ export declare function authenticateUser(request: any, env: any, context: Context): Promise<Response>;
4
+ export declare function isValidJwt(request: any, config: R2ExplorerConfig): Promise<any>;
@@ -0,0 +1,8 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
3
+ import { Context } from "../../interfaces";
4
+ import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
5
+ export declare class CreateFolder extends OpenAPIRoute {
6
+ static schema: OpenAPIRouteSchema;
7
+ handle(request: Request, env: any, context: Context, data: any): Promise<any>;
8
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
3
+ import { Context } from "../../interfaces";
4
+ import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
5
+ export declare class DeleteObject extends OpenAPIRoute {
6
+ static schema: OpenAPIRouteSchema;
7
+ handle(request: Request, env: any, context: Context, data: any): Promise<any>;
8
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
3
+ import { Context } from "../../interfaces";
4
+ import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
5
+ export declare class GetObject extends OpenAPIRoute {
6
+ static schema: OpenAPIRouteSchema;
7
+ handle(request: Request, env: any, context: Context, data: any): Promise<Response>;
8
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
3
+ import { Context } from "../../interfaces";
4
+ import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
5
+ export declare class HeadObject extends OpenAPIRoute {
6
+ static schema: OpenAPIRouteSchema;
7
+ handle(request: Request, env: any, context: Context, data: any): Promise<any>;
8
+ }
@@ -0,0 +1,10 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
3
+ import { Context } from "../../interfaces";
4
+ import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
5
+ export declare class ListBuckets extends OpenAPIRoute {
6
+ static schema: OpenAPIRouteSchema;
7
+ handle(request: Request, env: any, context: Context, data: any): Promise<{
8
+ buckets: any[];
9
+ }>;
10
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
3
+ import { Context } from "../../interfaces";
4
+ import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
5
+ export declare class ListObjects extends OpenAPIRoute {
6
+ static schema: OpenAPIRouteSchema;
7
+ handle(request: Request, env: any, context: Context, data: any): Promise<any>;
8
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
3
+ import { Context } from "../../interfaces";
4
+ import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
5
+ export declare class MoveObject extends OpenAPIRoute {
6
+ static schema: OpenAPIRouteSchema;
7
+ handle(request: Request, env: any, context: Context, data: any): Promise<any>;
8
+ }
@@ -0,0 +1,11 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
3
+ import { Context } from "../../../interfaces";
4
+ import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
5
+ export declare class CompleteUpload extends OpenAPIRoute {
6
+ static schema: OpenAPIRouteSchema;
7
+ handle(request: Request, env: any, context: Context, data: any): Promise<Response | {
8
+ success: boolean;
9
+ str: any;
10
+ }>;
11
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
3
+ import { Context } from "../../../interfaces";
4
+ import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
5
+ export declare class CreateUpload extends OpenAPIRoute {
6
+ static schema: OpenAPIRouteSchema;
7
+ handle(request: Request, env: any, context: Context, data: any): Promise<any>;
8
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
3
+ import { Context } from "../../../interfaces";
4
+ import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
5
+ export declare class PartUpload extends OpenAPIRoute {
6
+ static schema: OpenAPIRouteSchema;
7
+ handle(request: Request, env: any, context: Context, data: any): Promise<any>;
8
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
3
+ import { Context } from "../../interfaces";
4
+ import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
5
+ export declare class PutMetadata extends OpenAPIRoute {
6
+ static schema: OpenAPIRouteSchema;
7
+ handle(request: Request, env: any, context: Context, data: any): Promise<any>;
8
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
3
+ import { Context } from "../../interfaces";
4
+ import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
5
+ export declare class PutObject extends OpenAPIRoute {
6
+ static schema: OpenAPIRouteSchema;
7
+ handle(request: Request, env: any, context: Context, data: any): Promise<any>;
8
+ }
@@ -0,0 +1 @@
1
+ export declare const bucketsRouter: import("@cloudflare/itty-router-openapi").OpenAPIRouterType<import("@cloudflare/itty-router-openapi").Route, any[]>;
@@ -0,0 +1,2 @@
1
+ import { Context } from "./interfaces";
2
+ export declare function dashboardProxy(request: any, env: any, context: Context): Promise<any>;
@@ -0,0 +1,2 @@
1
+ export declare function getCurrentTimestampSeconds(): number;
2
+ export declare function getCurrentTimestampMilliseconds(): number;
@@ -0,0 +1,2 @@
1
+ import { Context } from "../interfaces";
2
+ export declare function receiveEmail(event: any, env: any, ctx: Context): Promise<void>;
@@ -0,0 +1,5 @@
1
+ import { R2ExplorerConfig } from "./interfaces";
2
+ export declare function R2Explorer(config?: R2ExplorerConfig): {
3
+ email(event: any, env: any, context: any): Promise<void>;
4
+ fetch(request: any, env: any, context: any): Promise<any>;
5
+ };
@@ -0,0 +1,22 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ export interface BasicAuth {
3
+ username: string;
4
+ password: string;
5
+ }
6
+ export interface R2ExplorerConfig {
7
+ readonly?: boolean;
8
+ cors?: boolean;
9
+ cfAccessTeamName?: string;
10
+ dashboardUrl?: string;
11
+ emailRouting?: {
12
+ targetBucket: string;
13
+ };
14
+ showHiddenFiles?: string;
15
+ cacheAssets?: boolean;
16
+ basicAuth?: BasicAuth | BasicAuth[];
17
+ }
18
+ export interface Context {
19
+ config: R2ExplorerConfig;
20
+ username?: string;
21
+ executionContext: ExecutionContext;
22
+ }
@@ -0,0 +1,14 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
3
+ import { Context } from "../../interfaces";
4
+ import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
5
+ export declare class GetInfo extends OpenAPIRoute {
6
+ static schema: OpenAPIRouteSchema;
7
+ handle(request: Request, env: any, context: Context, data: any): Promise<{
8
+ version: string;
9
+ config: import("../../interfaces").R2ExplorerConfig;
10
+ user: {
11
+ username: string;
12
+ };
13
+ }>;
14
+ }
@@ -0,0 +1 @@
1
+ export declare const serverRouter: import("@cloudflare/itty-router-openapi").OpenAPIRouterType<import("@cloudflare/itty-router-openapi").Route, any[]>;
@@ -0,0 +1,5 @@
1
+ export declare const config: {
2
+ raiseUnknownParameters: boolean;
3
+ generateOperationIds: boolean;
4
+ version: string;
5
+ };
package/package.json CHANGED
@@ -1,28 +1,48 @@
1
1
  {
2
2
  "name": "r2-explorer",
3
- "version": "0.6.0",
3
+ "version": "1.0.1",
4
4
  "description": "A Google Drive Interface for your Cloudflare R2 Buckets",
5
- "main": "dist/umd/index.js",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/src/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "dist"
18
+ ],
6
19
  "scripts": {
7
20
  "postinstallDisabled": "husky install",
8
21
  "prepublishOnly": "pinst --disable",
9
22
  "postpublish": "pinst --enable",
10
- "build": "npm run build:umd",
11
- "build:umd": "node tools/cleanup umd && rollup --config config/rollup.config.js",
23
+ "build": "node tools/cleanup umd && rollup --config rollup.config.js",
12
24
  "clean": "node tools/cleanup",
13
25
  "package": "npm run build && npm pack",
14
26
  "test": "jest --no-cache --runInBand",
15
27
  "test:cov": "jest --coverage --no-cache --runInBand",
16
28
  "addscope": "node tools/packagejson name @g4brym/r2-explorer",
17
29
  "prettify": "prettier . --write --ignore-unknown",
18
- "prepareDisabled": "husky install"
30
+ "prepareDisabled": "husky install",
31
+ "watch": "npm-watch build"
32
+ },
33
+ "watch": {
34
+ "build": {
35
+ "patterns": [
36
+ "src"
37
+ ],
38
+ "extensions": "js,ts",
39
+ "legacyWatch": true,
40
+ "runOnChangeOnly": false
41
+ }
19
42
  },
20
43
  "publishConfig": {
21
44
  "access": "public"
22
45
  },
23
- "files": [
24
- "dist"
25
- ],
26
46
  "keywords": [
27
47
  "cloudflare",
28
48
  "worker",
@@ -45,7 +65,7 @@
45
65
  ],
46
66
  "author": "Gabriel Massadas",
47
67
  "license": "MIT",
48
- "homepage": "https://github.com/G4brym/R2-Explorer",
68
+ "homepage": "https://r2explorer.dev",
49
69
  "repository": {
50
70
  "type": "git",
51
71
  "url": "git@github.com:G4brym/R2-Explorer.git"
@@ -54,30 +74,29 @@
54
74
  "url": "https://github.com/G4brym/R2-Explorer/issues"
55
75
  },
56
76
  "devDependencies": {
57
- "@commitlint/cli": "^13.1.0",
58
- "@commitlint/config-conventional": "^13.1.0",
59
- "@rollup/plugin-commonjs": "^17.0.0",
60
- "@rollup/plugin-json": "^5.0.1",
61
- "@rollup/plugin-node-resolve": "^11.1.0",
62
- "@types/jest": "^27.0.1",
63
- "@typescript-eslint/eslint-plugin": "^4.31.1",
64
- "@typescript-eslint/parser": "^4.31.1",
65
- "eslint": "^7.32.0",
66
- "eslint-config-prettier": "^8.3.0",
67
- "eslint-plugin-prettier": "^4.0.0",
68
- "husky": "^7.0.2",
69
- "jest": "^28.1.2",
77
+ "@cloudflare/workers-types": "^4.20230518.0",
78
+ "@rollup/plugin-terser": "^0.4.3",
79
+ "@rollup/plugin-typescript": "^11.1.2",
80
+ "@types/node": "^18.11.18",
81
+ "@types/service-worker-mock": "^2.0.1",
82
+ "eslint": "^8.18.0",
83
+ "eslint-config-prettier": "^8.5.0",
84
+ "eslint-config-typescript": "^3.0.0",
85
+ "husky": "^8.0.0",
86
+ "npm-watch": "^0.11.0",
70
87
  "pinst": "^2.1.6",
71
- "prettier": "^2.4.0",
88
+ "prettier": "^2.7.1",
72
89
  "rollup": "^2.36.1",
73
- "rollup-plugin-ts": "^3.0.2",
74
- "ts-jest": "^28.0.5",
75
- "ts-loader": "^9.2.5",
76
- "typescript": "^4.4.3",
77
- "wrangler": "^3.1.1"
90
+ "rollup-plugin-bundle-size": "^1.0.3",
91
+ "rollup-plugin-copy": "^3.4.0",
92
+ "service-worker-mock": "^2.0.5",
93
+ "ts-node": "^10.9.1",
94
+ "typescript": "^4.7.4",
95
+ "wrangler": "^3.6.0"
78
96
  },
79
97
  "dependencies": {
80
- "itty-router": "^2.6.1"
98
+ "@cloudflare/itty-router-openapi": "^1.0.1",
99
+ "postal-mime": "^1.0.16"
81
100
  },
82
101
  "bin": {
83
102
  "r2-explorer": "bin/r2-explorer.js"
@@ -1,70 +0,0 @@
1
- #! /usr/bin/env node
2
- var fs = require('fs')
3
-
4
- const args = process.argv.slice(2)
5
- const projectName = args[0] || 'r2-explorer'
6
-
7
- const dir = `./${projectName}`
8
- if (!fs.existsSync(dir)) {
9
- fs.mkdirSync(dir)
10
- }
11
-
12
- fs.writeFileSync(
13
- `${dir}/wrangler.toml`,
14
- `name = "${projectName}"
15
- compatibility_date = "2022-08-09"
16
- main = "src/index.js"
17
-
18
- [[r2_buckets]]
19
- binding = 'my-bucket-name'
20
- bucket_name = 'my-bucket-name'
21
- preview_bucket_name = 'my-bucket-name'
22
- `
23
- )
24
- fs.writeFileSync(
25
- `${dir}/package.json`,
26
- `{
27
- "name": "${projectName}",
28
- "version": "0.1.0",
29
- "private": true,
30
- "devDependencies": {
31
- "wrangler": "^2.4.2"
32
- },
33
- "scripts": {
34
- "publish": "wrangler publish"
35
- },
36
- "dependencies": {
37
- "r2-explorer": "^0.6.0"
38
- }
39
- }
40
-
41
- `
42
- )
43
-
44
- const srcDir = `${dir}/src`
45
- if (!fs.existsSync(srcDir)) {
46
- fs.mkdirSync(srcDir)
47
- }
48
-
49
- fs.writeFileSync(
50
- `${srcDir}/index.js`,
51
- `import { R2Explorer } from 'r2-explorer';
52
-
53
- const explorer = R2Explorer({ readonly: true })
54
-
55
- export default {
56
- async fetch(request, env, context) {
57
- return explorer(request, env, context)
58
- }
59
- };
60
- `
61
- )
62
-
63
- console.log(`Project ${projectName} successfully created!`)
64
- console.log('----------------------------')
65
- console.log('Next steps:')
66
- console.log(` 1. Run 'cd ${projectName} && npm install'`)
67
- console.log(" 2. Update the 'wrangler.toml' file with your R2 Buckets")
68
- console.log(" 3. Run 'wrangler publish' to deploy your own R2-Explorer!")
69
-
70
- process.exit(0) //no errors occurred