r2-explorer 1.0.6 → 1.1.0

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.
Files changed (83) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +9 -4
  3. package/dashboard/assets/AuthLayout.828e1213.js +1 -0
  4. package/dashboard/assets/EmailFilePage.148b4f84.js +2 -0
  5. package/dashboard/assets/EmailFilePage.c4890c9c.css +1 -0
  6. package/dashboard/assets/EmailFolderPage.0c5be156.js +1 -0
  7. package/dashboard/assets/EmailFolderPage.25044e0a.css +1 -0
  8. package/dashboard/assets/ErrorNotFound.a0d3ece6.js +1 -0
  9. package/dashboard/assets/FilesFolderPage.7cadc3fd.js +72 -0
  10. package/dashboard/assets/FilesFolderPage.ecacd99f.css +1 -0
  11. package/dashboard/assets/HomePage.fd1efdb0.js +1 -0
  12. package/dashboard/assets/KFOkCnqEu92Fr1MmgVxIIzQ.d240a9ae.woff +0 -0
  13. package/dashboard/assets/KFOlCnqEu92Fr1MmEU9fBBc-.6ba203eb.woff +0 -0
  14. package/dashboard/assets/KFOlCnqEu92Fr1MmSU5fBBc-.80684728.woff +0 -0
  15. package/dashboard/assets/KFOlCnqEu92Fr1MmWUlfBBc-.2df244f6.woff +0 -0
  16. package/dashboard/assets/KFOlCnqEu92Fr1MmYUtfBBc-.742ce02b.woff +0 -0
  17. package/dashboard/assets/KFOmCnqEu92Fr1Mu4mxM.f00fa16d.woff +0 -0
  18. package/dashboard/assets/LoginPage.5e2746c3.js +1 -0
  19. package/dashboard/assets/MainLayout.05d49d78.css +1 -0
  20. package/dashboard/assets/MainLayout.54c624c6.js +1 -0
  21. package/dashboard/assets/QCard.9ca85696.js +1 -0
  22. package/dashboard/assets/QCardActions.3d6ece78.js +1 -0
  23. package/dashboard/assets/QForm.1a0fa8bd.js +1 -0
  24. package/dashboard/assets/QInput.dbc14c53.js +1 -0
  25. package/dashboard/assets/QLayout.7c9341c3.js +1 -0
  26. package/dashboard/assets/QPage.1736cadc.js +1 -0
  27. package/dashboard/assets/QSeparator.d0c0fb0f.js +1 -0
  28. package/dashboard/assets/QSpace.3225ba0f.js +1 -0
  29. package/dashboard/assets/QTable.3fe6867d.js +1 -0
  30. package/dashboard/assets/QTd.32b217d9.js +1 -0
  31. package/dashboard/assets/auth-store.10a6215e.js +1 -0
  32. package/dashboard/assets/auth.3fb1cfd3.js +1 -0
  33. package/dashboard/assets/axios.d3fa833b.js +6 -0
  34. package/dashboard/assets/bus.def2db9e.js +1 -0
  35. package/dashboard/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.fd84f88b.woff +0 -0
  36. package/dashboard/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.4a4dbc62.woff2 +0 -0
  37. package/dashboard/assets/focus-manager.1ddae684.js +1 -0
  38. package/dashboard/assets/index.073c3cc5.css +5 -0
  39. package/dashboard/assets/index.31a4681e.js +25 -0
  40. package/dashboard/assets/main-store.ee636e78.js +1 -0
  41. package/dashboard/assets/scroll.6727d3ea.js +1 -0
  42. package/dashboard/assets/use-checkbox.687a19bf.js +1 -0
  43. package/dashboard/assets/use-dark.6e19ce43.js +1 -0
  44. package/dashboard/assets/use-quasar.0c0b766f.js +1 -0
  45. package/dashboard/assets/use-transition.19980487.js +1 -0
  46. package/dashboard/favicon.ico +0 -0
  47. package/dashboard/icons/favicon-128x128.png +0 -0
  48. package/dashboard/icons/favicon-16x16.png +0 -0
  49. package/dashboard/icons/favicon-32x32.png +0 -0
  50. package/dashboard/icons/favicon-96x96.png +0 -0
  51. package/dashboard/icons/logo.png +0 -0
  52. package/dashboard/index.html +3 -0
  53. package/dashboard/logo-white.svg +5 -0
  54. package/dashboard/logo.png +0 -0
  55. package/dashboard/robots.txt +2 -0
  56. package/dist/index.d.mts +33 -0
  57. package/dist/index.d.ts +33 -0
  58. package/dist/index.js +802 -1
  59. package/dist/index.mjs +771 -1
  60. package/package.json +22 -60
  61. package/dist/src/authentication/api/access.d.ts +0 -4
  62. package/dist/src/authentication/api/basic.d.ts +0 -3
  63. package/dist/src/buckets/api/createFolder.d.ts +0 -8
  64. package/dist/src/buckets/api/deleteObject.d.ts +0 -8
  65. package/dist/src/buckets/api/getObject.d.ts +0 -8
  66. package/dist/src/buckets/api/headObject.d.ts +0 -8
  67. package/dist/src/buckets/api/listBuckets.d.ts +0 -10
  68. package/dist/src/buckets/api/listObjects.d.ts +0 -8
  69. package/dist/src/buckets/api/moveObject.d.ts +0 -8
  70. package/dist/src/buckets/api/multipart/completeUpload.d.ts +0 -11
  71. package/dist/src/buckets/api/multipart/createUpload.d.ts +0 -8
  72. package/dist/src/buckets/api/multipart/partUpload.d.ts +0 -8
  73. package/dist/src/buckets/api/putMetadata.d.ts +0 -8
  74. package/dist/src/buckets/api/putObject.d.ts +0 -8
  75. package/dist/src/buckets/router.d.ts +0 -1
  76. package/dist/src/dashbord.d.ts +0 -2
  77. package/dist/src/dates.d.ts +0 -2
  78. package/dist/src/emails/receiveEmail.d.ts +0 -2
  79. package/dist/src/index.d.ts +0 -5
  80. package/dist/src/interfaces.d.ts +0 -22
  81. package/dist/src/server/api/getInfo.d.ts +0 -25
  82. package/dist/src/server/router.d.ts +0 -1
  83. package/dist/src/settings.d.ts +0 -5
package/dist/index.mjs CHANGED
@@ -1 +1,771 @@
1
- import{createCors as e}from"itty-router";import{OpenAPIRoute as t,Path as a,Query as s,RequestBody as o,Int as r,OpenAPIRouter as n}from"@cloudflare/itty-router-openapi";import{z as c}from"zod";const i=!0,u=!1,d="1.0.6";class p extends t{static schema={operationId:"get-bucket-list-objects",tags:["Buckets"],summary:"List objects",parameters:{bucket:a(String),limit:s(Number,{required:!1}),prefix:s(c.string().optional().describe("base64 encoded prefix")),cursor:s(c.string().optional()),delimiter:s(c.string().optional()),include:s(c.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 l extends t{static schema={operationId:"post-bucket-move-object",tags:["Buckets"],summary:"Move object",parameters:{bucket:a(String)},requestBody:{oldKey:c.string().describe("base64 encoded file key"),newKey:c.string().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],r=decodeURIComponent(escape(atob(s.body.oldKey))),n=decodeURIComponent(escape(atob(s.body.newKey))),c=await o.get(r),i=await o.put(n,c.body,{customMetadata:c.customMetadata,httpMetadata:c.httpMetadata});return await o.delete(r),i}}class m extends t{static schema={operationId:"post-bucket-create-folder",tags:["Buckets"],summary:"Create folder",parameters:{bucket:a(String)},requestBody:{key:c.string().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],r=decodeURIComponent(escape(atob(s.body.key)));return await o.put(r,"R2 Explorer Folder")}}class y extends t{static schema={operationId:"post-bucket-upload-object",tags:["Buckets"],summary:"Upload object",requestBody:new o({content:{"application/octet-stream":{schema:{type:"string",format:"binary"}}}}),parameters:{bucket:a(String),key:s(c.string().describe("base64 encoded file key")),customMetadata:s(c.string().optional().describe("base64 encoded json string")),httpMetadata:s(c.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 r,n,c=decodeURIComponent(escape(atob(s.query.key)));return s.query.customMetadata&&(r=JSON.parse(decodeURIComponent(escape(atob(s.query.customMetadata))))),s.query.httpMetadata&&(n=JSON.parse(decodeURIComponent(escape(atob(s.query.httpMetadata))))),await o.put(c,e.body,{customMetadata:r,httpMetadata:n})}}class b extends t{static schema={operationId:"post-bucket-delete-object",tags:["Buckets"],summary:"Delete object",parameters:{bucket:a(String)},requestBody:{key:c.string().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],r=decodeURIComponent(escape(atob(s.body.key)));return await o.delete(r),{}}}class g extends t{static schema={operationId:"post-multipart-create-upload",tags:["Multipart"],summary:"Create upload",parameters:{bucket:a(String),key:s(c.string().describe("base64 encoded file key")),customMetadata:s(c.string().optional().describe("base64 encoded json string")),httpMetadata:s(c.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],r=decodeURIComponent(escape(atob(s.query.key)));let n,c;return s.query.customMetadata&&(n=JSON.parse(decodeURIComponent(escape(atob(s.query.customMetadata))))),s.query.httpMetadata&&(c=JSON.parse(decodeURIComponent(escape(atob(s.query.httpMetadata))))),await o.createMultipartUpload(r,{customMetadata:n,httpMetadata:c})}}class f extends t{static schema={operationId:"post-multipart-part-upload",tags:["Multipart"],summary:"Part upload",requestBody:new o({content:{"application/octet-stream":{schema:{type:"string",format:"binary"}}}}),parameters:{bucket:a(String),key:s(c.string().describe("base64 encoded file key")),uploadId:s(String),partNumber:s(r)}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],r=decodeURIComponent(escape(atob(s.query.key))),n=o.resumeMultipartUpload(r,s.query.uploadId);try{return await n.uploadPart(s.query.partNumber,e.body)}catch(e){return new Response(e.message,{status:400})}}}class h extends t{static schema={operationId:"post-multipart-complete-upload",tags:["Multipart"],summary:"Complete upload",parameters:{bucket:a(String)},requestBody:{uploadId:String,key:c.string().describe("base64 encoded file key"),parts:[{etag:String,partNumber:r}]}};async handle(e,t,a,s){if(!0===a.config.readonly)return Response.json({msg:"unauthorized"},{status:401});const o=t[s.params.bucket],r=s.body.uploadId,n=decodeURIComponent(escape(atob(s.body.key))),c=s.body.parts,i=await o.resumeMultipartUpload(n,r);try{return{success:!0,str:await i.complete(c)}}catch(e){return Response.json({msg:e.message},{status:400})}}}class k extends t{static schema={operationId:"get-bucket-object",tags:["Buckets"],summary:"Get Object",parameters:{bucket:a(String),key:a(c.string().describe("base64 encoded file key"))},responses:{200:{description:"File binary",schema:c.string().openapi({format:"binary"})}}};async handle(e,t,a,s){const o=t[s.params.bucket],r=decodeURIComponent(escape(atob(s.params.key))),n=await o.get(r);if(null===n)return Response.json({msg:"Object Not Found"},{status:404});const c=new Headers;return n.writeHttpMetadata(c),c.set("etag",n.httpEtag),c.set("Content-Disposition",`attachment; filename="${r.split("/").pop()}"`),new Response(n.body,{headers:c})}}class w extends t{static schema={operationId:"Head-bucket-object",tags:["Buckets"],summary:"Get Object",parameters:{bucket:a(String),key:s(c.string().describe("base64 encoded file key"))}};async handle(e,t,a,s){const o=t[s.params.bucket],r=decodeURIComponent(escape(atob(s.params.key))),n=await o.head(r);return null===n?Response.json({msg:"Object Not Found"},{status:404}):n}}class j extends t{static schema={operationId:"post-bucket-put-object-metadata",tags:["Buckets"],summary:"Update object metadata",parameters:{bucket:a(String),key:a(c.string().describe("base64 encoded file key"))},requestBody:{customMetadata:c.record(c.string(),c.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],r=decodeURIComponent(escape(atob(s.params.key))),n=await o.get(r);return await o.put(r,n.body,{customMetadata:s.body.customMetadata})}}const R=n({base:"/api/buckets",raiseUnknownParameters:i,generateOperationIds:u});R.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}}}),R.get("/:bucket",p),R.post("/:bucket/move",l),R.post("/:bucket/folder",m),R.post("/:bucket/upload",y),R.post("/:bucket/multipart/create",g),R.post("/:bucket/multipart/upload",f),R.post("/:bucket/multipart/complete",h),R.post("/:bucket/delete",b),R.head("/:bucket/:key",w),R.get("/:bucket/:key",k),R.post("/:bucket/:key",j);let I={},x=0;function M(e){return`https://${e}.cloudflareaccess.com`}async function C(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(I).length||Math.floor(Date.now()/1e3)<x)&&(I=await async function(e){let t=`${M(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),r=new Date(Date.now());if(o<=r)return console.log("expired token"),!1;if(s.payload?.iss!==M(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(I)){if(await S(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 S(e,t,a){return crypto.subtle.verify("RSASSA-PKCS1-v1_5",e,t,a)}async function U(e,t,a){const s=caches.default;let o,r=new URL(e.url).pathname;if(r.includes(".")||(r="/"),!1!==a.config.cacheAssets&&(o=await s.match(e),o))return o;let n="https://demo.r2explorer.dev";a.config?.dashboardUrl&&(n=a.config.dashboardUrl.endsWith("/")?a.config.dashboardUrl.slice(0,-1):a.config.dashboardUrl);const c=await fetch(`${n}${r}`);return o=new Response(await c.body,{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 A=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),r=new A,n=await r.parse(o),c=`${Math.floor(Date.now())}-${crypto.randomUUID()}`;await s.put(`.r2-explorer/emails/inbox/${c}.json`,JSON.stringify(n),{customMetadata:{subject:n.subject,from_address:n.from?.address,from_name:n.from?.name,to_address:n.to.length>0?n.to[0].address:null,to_name:n.to.length>0?n.to[0].name:null,has_attachments:n.attachments.length>0,read:!1,timestamp:Date.now()}});for(const e of n.attachments)await s.put(`.r2-explorer/emails/inbox/${c}/${e.filename}`,e.content)}const B=n({base:"/api/server",raiseUnknownParameters:i,generateOperationIds:u});async function v(e,t,a){let s,o=!1;try{o=function(e){const t=e.headers.get("Authorization");if(!t)return null;return atob(t.replace("Basic","").trim())}(e).split(":"),Array.isArray(a.config.basicAuth)||(a.config.basicAuth=[a.config.basicAuth]);for(const e of a.config.basicAuth)if(e.username===o[0]&&e.password===o[1]){s=e.username;break}}catch(e){}if(!1===o||void 0===s)return Response.json({success:!1,errors:[{code:1e4,message:"Authentication error! This server requires Basic Auth"}]},{status:401});a.username=s}function O(t){!1!==(t=t||{}).readonly&&(t.readonly=!0);const a={info:{title:"R2 Explorer API",version:d}};t.basicAuth&&(a.security=[{basicAuth:[]}]);const s=n({schema:a}),{preflight:o,corsify:r}=e();return!0===t.cors&&s.all("/api*",o),t.cfAccessTeamName&&s.all("*",C),t.basicAuth&&(s.registry.registerComponent("securitySchemes","basicAuth",{type:"http",scheme:"basic"}),s.all("/api*",v)),s.all("/api/server/*",B),s.all("/api/buckets/*",R),s.original.get("*",U),s.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,a,o){let n=await s.handle(e,a,{executionContext:o,config:t});return!0===t.cors&&(n=r(n)),n}}}B.get("/config",class extends t{static schema={operationId:"get-server-info",tags:["Server"],summary:"Get server info"};async handle(e,t,a,s){const o={...a.config};return delete o.basicAuth,{version:d,config:o,user:{username:a.username}}}});export{O as R2Explorer};
1
+ // src/index.ts
2
+ import { cloudflareAccess } from "@hono/cloudflare-access";
3
+ import {
4
+ extendZodWithOpenApi,
5
+ fromHono
6
+ } from "chanfana";
7
+ import { Hono } from "hono";
8
+ import { basicAuth } from "hono/basic-auth";
9
+ import { cors } from "hono/cors";
10
+ import { z as z13 } from "zod";
11
+
12
+ // src/foundation/middlewares/readonly.ts
13
+ async function readOnlyMiddleware(c, next) {
14
+ const config = c.get("config");
15
+ if (config.readonly === true && !["GET", "HEAD"].includes(c.req.method)) {
16
+ return Response.json(
17
+ {
18
+ success: false,
19
+ errors: [
20
+ {
21
+ code: 10005,
22
+ message: "This instance is in ReadOnly Mode, no changes are allowed!"
23
+ }
24
+ ]
25
+ },
26
+ { status: 401 }
27
+ );
28
+ }
29
+ await next();
30
+ }
31
+
32
+ // package.json
33
+ var version = "1.1.0";
34
+
35
+ // src/foundation/settings.ts
36
+ var settings = {
37
+ version
38
+ };
39
+
40
+ // src/modules/buckets/createFolder.ts
41
+ import { OpenAPIRoute } from "chanfana";
42
+ import { z } from "zod";
43
+ var CreateFolder = class extends OpenAPIRoute {
44
+ schema = {
45
+ operationId: "post-bucket-create-folder",
46
+ tags: ["Buckets"],
47
+ summary: "Create folder",
48
+ request: {
49
+ params: z.object({
50
+ bucket: z.string()
51
+ }),
52
+ body: {
53
+ content: {
54
+ "application/json": {
55
+ schema: z.object({
56
+ key: z.string().describe("base64 encoded file key")
57
+ })
58
+ }
59
+ }
60
+ }
61
+ }
62
+ };
63
+ async handle(c) {
64
+ const data = await this.getValidatedData();
65
+ const bucket = c.env[data.params.bucket];
66
+ const key = decodeURIComponent(escape(atob(data.body.key)));
67
+ return await bucket.put(key, "R2 Explorer Folder");
68
+ }
69
+ };
70
+
71
+ // src/modules/buckets/deleteObject.ts
72
+ import { OpenAPIRoute as OpenAPIRoute2 } from "chanfana";
73
+ import { z as z2 } from "zod";
74
+ var DeleteObject = class extends OpenAPIRoute2 {
75
+ schema = {
76
+ operationId: "post-bucket-delete-object",
77
+ tags: ["Buckets"],
78
+ summary: "Delete object",
79
+ request: {
80
+ params: z2.object({
81
+ bucket: z2.string()
82
+ }),
83
+ body: {
84
+ content: {
85
+ "application/json": {
86
+ schema: z2.object({
87
+ key: z2.string().describe("base64 encoded file key")
88
+ })
89
+ }
90
+ }
91
+ }
92
+ }
93
+ };
94
+ async handle(c) {
95
+ const data = await this.getValidatedData();
96
+ const bucket = c.env[data.params.bucket];
97
+ const key = decodeURIComponent(escape(atob(data.body.key)));
98
+ await bucket.delete(key);
99
+ return { success: true };
100
+ }
101
+ };
102
+
103
+ // src/modules/buckets/getObject.ts
104
+ import { OpenAPIRoute as OpenAPIRoute3 } from "chanfana";
105
+ import { z as z3 } from "zod";
106
+ var GetObject = class extends OpenAPIRoute3 {
107
+ schema = {
108
+ operationId: "get-bucket-object",
109
+ tags: ["Buckets"],
110
+ summary: "Get Object",
111
+ request: {
112
+ params: z3.object({
113
+ bucket: z3.string(),
114
+ key: z3.string().describe("base64 encoded file key")
115
+ })
116
+ },
117
+ responses: {
118
+ "200": {
119
+ description: "File binary",
120
+ schema: z3.string().openapi({ format: "binary" })
121
+ }
122
+ }
123
+ };
124
+ async handle(c) {
125
+ const data = await this.getValidatedData();
126
+ const bucket = c.env[data.params.bucket];
127
+ let filePath;
128
+ try {
129
+ filePath = decodeURIComponent(escape(atob(data.params.key)));
130
+ } catch (e) {
131
+ filePath = decodeURIComponent(
132
+ escape(atob(decodeURIComponent(data.params.key)))
133
+ );
134
+ }
135
+ const object = await bucket.get(filePath);
136
+ if (object === null) {
137
+ return Response.json({ msg: "Object Not Found" }, { status: 404 });
138
+ }
139
+ const headers = new Headers();
140
+ object.writeHttpMetadata(headers);
141
+ headers.set("etag", object.httpEtag);
142
+ headers.set(
143
+ "Content-Disposition",
144
+ `attachment; filename="${filePath.split("/").pop()}"`
145
+ );
146
+ return new Response(object.body, {
147
+ headers
148
+ });
149
+ }
150
+ };
151
+
152
+ // src/modules/buckets/headObject.ts
153
+ import { OpenAPIRoute as OpenAPIRoute4 } from "chanfana";
154
+ import { z as z4 } from "zod";
155
+ var HeadObject = class extends OpenAPIRoute4 {
156
+ schema = {
157
+ operationId: "Head-bucket-object",
158
+ tags: ["Buckets"],
159
+ summary: "Get Object",
160
+ request: {
161
+ params: z4.object({
162
+ bucket: z4.string(),
163
+ key: z4.string().describe("base64 encoded file key")
164
+ })
165
+ }
166
+ };
167
+ async handle(c) {
168
+ const data = await this.getValidatedData();
169
+ const bucket = c.env[data.params.bucket];
170
+ let filePath;
171
+ try {
172
+ filePath = decodeURIComponent(escape(atob(data.params.key)));
173
+ } catch (e) {
174
+ filePath = decodeURIComponent(
175
+ escape(atob(decodeURIComponent(data.params.key)))
176
+ );
177
+ }
178
+ const object = await bucket.head(filePath);
179
+ if (object === null) {
180
+ return Response.json({ msg: "Object Not Found" }, { status: 404 });
181
+ }
182
+ return object;
183
+ }
184
+ };
185
+
186
+ // src/modules/buckets/listBuckets.ts
187
+ import { OpenAPIRoute as OpenAPIRoute5 } from "chanfana";
188
+ var ListBuckets = class extends OpenAPIRoute5 {
189
+ schema = {
190
+ operationId: "get-bucket-list",
191
+ tags: ["Buckets"],
192
+ summary: "List buckets"
193
+ };
194
+ async handle(c) {
195
+ const buckets = [];
196
+ for (const [key, value] of Object.entries(c.env)) {
197
+ if (value.get && value.put && value.get.toString().includes("function") && value.put.toString().includes("function")) {
198
+ buckets.push({ name: key });
199
+ }
200
+ }
201
+ return {
202
+ buckets
203
+ };
204
+ }
205
+ };
206
+
207
+ // src/modules/buckets/listObjects.ts
208
+ import { OpenAPIRoute as OpenAPIRoute6 } from "chanfana";
209
+ import { z as z5 } from "zod";
210
+ var ListObjects = class extends OpenAPIRoute6 {
211
+ schema = {
212
+ operationId: "get-bucket-list-objects",
213
+ tags: ["Buckets"],
214
+ summary: "List objects",
215
+ request: {
216
+ params: z5.object({
217
+ bucket: z5.string()
218
+ }),
219
+ query: z5.object({
220
+ limit: z5.number().optional(),
221
+ prefix: z5.string().nullable().optional().describe("base64 encoded prefix"),
222
+ cursor: z5.string().nullable().optional(),
223
+ delimiter: z5.string().nullable().optional(),
224
+ startAfter: z5.string().nullable().optional(),
225
+ include: z5.enum(["httpMetadata", "customMetadata"]).array().optional()
226
+ })
227
+ }
228
+ };
229
+ async handle(c) {
230
+ const data = await this.getValidatedData();
231
+ const bucket = c.env[data.params.bucket];
232
+ c.header("Access-Control-Allow-Credentials", "asads");
233
+ return await bucket.list({
234
+ limit: data.query.limit,
235
+ prefix: data.query.prefix ? decodeURIComponent(escape(atob(data.query.prefix))) : void 0,
236
+ cursor: data.query.cursor,
237
+ startAfter: data.query.startAfter,
238
+ delimiter: data.query.delimiter ? data.query.delimiter : "",
239
+ // @ts-ignore
240
+ include: data.query.include
241
+ });
242
+ }
243
+ };
244
+
245
+ // src/modules/buckets/moveObject.ts
246
+ import { OpenAPIRoute as OpenAPIRoute7 } from "chanfana";
247
+ import { z as z6 } from "zod";
248
+ var MoveObject = class extends OpenAPIRoute7 {
249
+ schema = {
250
+ operationId: "post-bucket-move-object",
251
+ tags: ["Buckets"],
252
+ summary: "Move object",
253
+ request: {
254
+ params: z6.object({
255
+ bucket: z6.string()
256
+ }),
257
+ body: {
258
+ content: {
259
+ "application/json": {
260
+ schema: z6.object({
261
+ oldKey: z6.string().describe("base64 encoded file key"),
262
+ newKey: z6.string().describe("base64 encoded file key")
263
+ })
264
+ }
265
+ }
266
+ }
267
+ }
268
+ };
269
+ async handle(c) {
270
+ const data = await this.getValidatedData();
271
+ const bucket = c.env[data.params.bucket];
272
+ const oldKey = decodeURIComponent(escape(atob(data.body.oldKey)));
273
+ const newKey = decodeURIComponent(escape(atob(data.body.newKey)));
274
+ const object = await bucket.get(oldKey);
275
+ const resp = await bucket.put(newKey, object.body, {
276
+ customMetadata: object.customMetadata,
277
+ httpMetadata: object.httpMetadata
278
+ });
279
+ await bucket.delete(oldKey);
280
+ return resp;
281
+ }
282
+ };
283
+
284
+ // src/modules/buckets/multipart/completeUpload.ts
285
+ import { OpenAPIRoute as OpenAPIRoute8 } from "chanfana";
286
+ import { z as z7 } from "zod";
287
+ var CompleteUpload = class extends OpenAPIRoute8 {
288
+ schema = {
289
+ operationId: "post-multipart-complete-upload",
290
+ tags: ["Multipart"],
291
+ summary: "Complete upload",
292
+ request: {
293
+ params: z7.object({
294
+ bucket: z7.string()
295
+ }),
296
+ body: {
297
+ content: {
298
+ "application/json": {
299
+ schema: z7.object({
300
+ uploadId: z7.string(),
301
+ parts: z7.object({
302
+ etag: z7.string(),
303
+ partNumber: z7.number().int()
304
+ }).array(),
305
+ key: z7.string().describe("base64 encoded file key")
306
+ })
307
+ }
308
+ }
309
+ }
310
+ }
311
+ };
312
+ async handle(c) {
313
+ const data = await this.getValidatedData();
314
+ const bucket = c.env[data.params.bucket];
315
+ const uploadId = data.body.uploadId;
316
+ const key = decodeURIComponent(escape(atob(data.body.key)));
317
+ const parts = data.body.parts;
318
+ const multipartUpload = await bucket.resumeMultipartUpload(key, uploadId);
319
+ try {
320
+ const resp = await multipartUpload.complete(parts);
321
+ return {
322
+ success: true,
323
+ str: resp
324
+ };
325
+ } catch (error) {
326
+ return Response.json({ msg: error.message }, { status: 400 });
327
+ }
328
+ }
329
+ };
330
+
331
+ // src/modules/buckets/multipart/createUpload.ts
332
+ import { OpenAPIRoute as OpenAPIRoute9 } from "chanfana";
333
+ import { z as z8 } from "zod";
334
+ var CreateUpload = class extends OpenAPIRoute9 {
335
+ schema = {
336
+ operationId: "post-multipart-create-upload",
337
+ tags: ["Multipart"],
338
+ summary: "Create upload",
339
+ request: {
340
+ params: z8.object({
341
+ bucket: z8.string()
342
+ }),
343
+ query: z8.object({
344
+ key: z8.string().describe("base64 encoded file key"),
345
+ customMetadata: z8.string().nullable().optional().describe("base64 encoded json string"),
346
+ httpMetadata: z8.string().nullable().optional().describe("base64 encoded json string")
347
+ })
348
+ }
349
+ };
350
+ async handle(c) {
351
+ const data = await this.getValidatedData();
352
+ const bucket = c.env[data.params.bucket];
353
+ const key = decodeURIComponent(escape(atob(data.query.key)));
354
+ let customMetadata = void 0;
355
+ if (data.query.customMetadata) {
356
+ customMetadata = JSON.parse(
357
+ decodeURIComponent(escape(atob(data.query.customMetadata)))
358
+ );
359
+ }
360
+ let httpMetadata = void 0;
361
+ if (data.query.httpMetadata) {
362
+ httpMetadata = JSON.parse(
363
+ decodeURIComponent(escape(atob(data.query.httpMetadata)))
364
+ );
365
+ }
366
+ return await bucket.createMultipartUpload(key, {
367
+ customMetadata,
368
+ httpMetadata
369
+ });
370
+ }
371
+ };
372
+
373
+ // src/modules/buckets/multipart/partUpload.ts
374
+ import { OpenAPIRoute as OpenAPIRoute10 } from "chanfana";
375
+ import { z as z9 } from "zod";
376
+ var PartUpload = class extends OpenAPIRoute10 {
377
+ schema = {
378
+ operationId: "post-multipart-part-upload",
379
+ tags: ["Multipart"],
380
+ summary: "Part upload",
381
+ request: {
382
+ body: {
383
+ content: {
384
+ "application/octet-stream": {
385
+ schema: z9.object({}).openapi({
386
+ type: "string",
387
+ format: "binary"
388
+ })
389
+ }
390
+ }
391
+ },
392
+ params: z9.object({
393
+ bucket: z9.string()
394
+ }),
395
+ query: z9.object({
396
+ key: z9.string().describe("base64 encoded file key"),
397
+ uploadId: z9.string(),
398
+ partNumber: z9.number().int()
399
+ })
400
+ }
401
+ };
402
+ async handle(c) {
403
+ const data = await this.getValidatedData();
404
+ const bucket = c.env[data.params.bucket];
405
+ const key = decodeURIComponent(escape(atob(data.query.key)));
406
+ const multipartUpload = bucket.resumeMultipartUpload(
407
+ key,
408
+ data.query.uploadId
409
+ );
410
+ try {
411
+ return await multipartUpload.uploadPart(
412
+ data.query.partNumber,
413
+ c.req.raw.body
414
+ );
415
+ } catch (error) {
416
+ return new Response(error.message, { status: 400 });
417
+ }
418
+ }
419
+ };
420
+
421
+ // src/modules/buckets/putMetadata.ts
422
+ import { OpenAPIRoute as OpenAPIRoute11 } from "chanfana";
423
+ import { z as z10 } from "zod";
424
+ var PutMetadata = class extends OpenAPIRoute11 {
425
+ schema = {
426
+ operationId: "post-bucket-put-object-metadata",
427
+ tags: ["Buckets"],
428
+ summary: "Update object metadata",
429
+ request: {
430
+ params: z10.object({
431
+ bucket: z10.string(),
432
+ key: z10.string().describe("base64 encoded file key")
433
+ }),
434
+ body: {
435
+ content: {
436
+ "application/json": {
437
+ schema: z10.object({
438
+ customMetadata: z10.record(z10.string(), z10.any())
439
+ }).openapi("Object metadata")
440
+ }
441
+ }
442
+ }
443
+ }
444
+ };
445
+ async handle(c) {
446
+ const data = await this.getValidatedData();
447
+ const bucket = c.env[data.params.bucket];
448
+ let filePath;
449
+ try {
450
+ filePath = decodeURIComponent(escape(atob(data.params.key)));
451
+ } catch (e) {
452
+ filePath = decodeURIComponent(
453
+ escape(atob(decodeURIComponent(data.params.key)))
454
+ );
455
+ }
456
+ const object = await bucket.get(filePath);
457
+ return await bucket.put(filePath, object.body, {
458
+ customMetadata: data.body.customMetadata
459
+ });
460
+ }
461
+ };
462
+
463
+ // src/modules/buckets/putObject.ts
464
+ import { OpenAPIRoute as OpenAPIRoute12 } from "chanfana";
465
+ import { z as z11 } from "zod";
466
+ var PutObject = class extends OpenAPIRoute12 {
467
+ schema = {
468
+ operationId: "post-bucket-upload-object",
469
+ tags: ["Buckets"],
470
+ summary: "Upload object",
471
+ request: {
472
+ body: {
473
+ content: {
474
+ "application/octet-stream": {
475
+ schema: z11.object({}).openapi({
476
+ type: "string",
477
+ format: "binary"
478
+ })
479
+ }
480
+ }
481
+ },
482
+ params: z11.object({
483
+ bucket: z11.string()
484
+ }),
485
+ query: z11.object({
486
+ key: z11.string().describe("base64 encoded file key"),
487
+ customMetadata: z11.string().nullable().optional().describe("base64 encoded json string"),
488
+ httpMetadata: z11.string().nullable().optional().describe("base64 encoded json string")
489
+ })
490
+ }
491
+ };
492
+ async handle(c) {
493
+ const data = await this.getValidatedData();
494
+ const bucket = c.env[data.params.bucket];
495
+ const key = decodeURIComponent(escape(atob(data.query.key)));
496
+ let customMetadata = void 0;
497
+ if (data.query.customMetadata) {
498
+ customMetadata = JSON.parse(
499
+ decodeURIComponent(escape(atob(data.query.customMetadata)))
500
+ );
501
+ }
502
+ let httpMetadata = void 0;
503
+ if (data.query.httpMetadata) {
504
+ httpMetadata = JSON.parse(
505
+ decodeURIComponent(escape(atob(data.query.httpMetadata)))
506
+ );
507
+ }
508
+ return await bucket.put(key, c.req.raw.body, {
509
+ customMetadata,
510
+ httpMetadata
511
+ });
512
+ }
513
+ };
514
+
515
+ // src/modules/dashboard.ts
516
+ function dashboardIndex(c) {
517
+ if (c.env.ASSETS === void 0) {
518
+ return c.text(
519
+ "ASSETS binding is not defined, learn more here: https://r2explorer.dev/guides/migrating-to-1.1/",
520
+ 500
521
+ );
522
+ }
523
+ return c.text(
524
+ "ASSETS binding is not pointing to a valid dashboard, learn more here: https://r2explorer.dev/guides/migrating-to-1.1/",
525
+ 500
526
+ );
527
+ }
528
+ async function dashboardRedirect(c, next) {
529
+ if (c.env.ASSETS === void 0) {
530
+ return c.text(
531
+ "ASSETS binding is not defined, learn more here: https://r2explorer.dev/guides/migrating-to-1.1/",
532
+ 500
533
+ );
534
+ }
535
+ const url = new URL(c.req.url);
536
+ if (!url.pathname.includes(".")) {
537
+ return c.env.ASSETS.fetch(new Request(url.origin));
538
+ }
539
+ await next();
540
+ }
541
+
542
+ // src/modules/emails/receiveEmail.ts
543
+ import PostalMime from "postal-mime";
544
+
545
+ // src/foundation/dates.ts
546
+ function getCurrentTimestampMilliseconds() {
547
+ return Math.floor(Date.now());
548
+ }
549
+
550
+ // src/modules/emails/receiveEmail.ts
551
+ async function streamToArrayBuffer(stream, streamSize) {
552
+ const result = new Uint8Array(streamSize);
553
+ let bytesRead = 0;
554
+ const reader = stream.getReader();
555
+ while (true) {
556
+ const { done, value } = await reader.read();
557
+ if (done) {
558
+ break;
559
+ }
560
+ result.set(value, bytesRead);
561
+ bytesRead += value.length;
562
+ }
563
+ return result;
564
+ }
565
+ async function receiveEmail(event, env, ctx, config) {
566
+ let bucket;
567
+ if (config?.emailRouting?.targetBucket && env[config.emailRouting.targetBucket]) {
568
+ bucket = env[config.emailRouting.targetBucket];
569
+ }
570
+ if (!bucket) {
571
+ for (const [key, value] of Object.entries(env)) {
572
+ if (value.get && value.put) {
573
+ bucket = value;
574
+ break;
575
+ }
576
+ }
577
+ }
578
+ const rawEmail = await streamToArrayBuffer(event.raw, event.rawSize);
579
+ const parser = new PostalMime();
580
+ const parsedEmail = await parser.parse(rawEmail);
581
+ const emailPath = `${getCurrentTimestampMilliseconds()}-${crypto.randomUUID()}`;
582
+ await bucket.put(
583
+ `.r2-explorer/emails/inbox/${emailPath}.json`,
584
+ JSON.stringify(parsedEmail),
585
+ {
586
+ customMetadata: {
587
+ subject: parsedEmail.subject,
588
+ from_address: parsedEmail.from?.address,
589
+ from_name: parsedEmail.from?.name,
590
+ to_address: parsedEmail.to.length > 0 ? parsedEmail.to[0].address : null,
591
+ to_name: parsedEmail.to.length > 0 ? parsedEmail.to[0].name : null,
592
+ has_attachments: parsedEmail.attachments.length > 0,
593
+ read: false,
594
+ timestamp: Date.now()
595
+ }
596
+ }
597
+ );
598
+ for (const att of parsedEmail.attachments) {
599
+ await bucket.put(
600
+ `.r2-explorer/emails/inbox/${emailPath}/${att.filename}`,
601
+ att.content
602
+ );
603
+ }
604
+ }
605
+
606
+ // src/modules/emails/sendEmail.ts
607
+ import { OpenAPIRoute as OpenAPIRoute13, Str } from "chanfana";
608
+ import { z as z12 } from "zod";
609
+ var SendEmail = class extends OpenAPIRoute13 {
610
+ schema = {
611
+ operationId: "post-email-send",
612
+ tags: ["Emails"],
613
+ summary: "Send Email",
614
+ request: {
615
+ body: {
616
+ content: {
617
+ "application/json": {
618
+ schema: z12.object({
619
+ subject: Str({ example: "Look! No servers" }),
620
+ from: z12.object({
621
+ email: Str({ example: "sender@example.com" }),
622
+ name: Str({ example: "Workers - MailChannels integration" })
623
+ }),
624
+ to: z12.object({
625
+ email: Str({ example: "test@example.com" }),
626
+ name: Str({ example: "Test Recipient" })
627
+ }).array(),
628
+ content: z12.object({}).catchall(z12.string())
629
+ })
630
+ }
631
+ }
632
+ }
633
+ }
634
+ };
635
+ async handle(c) {
636
+ if (c.get("config").readonly === true)
637
+ return Response.json({ msg: "unauthorized" }, { status: 401 });
638
+ return {
639
+ success: false,
640
+ error: "unavailable"
641
+ };
642
+ }
643
+ };
644
+
645
+ // src/modules/server/getInfo.ts
646
+ import { OpenAPIRoute as OpenAPIRoute14 } from "chanfana";
647
+ var GetInfo = class extends OpenAPIRoute14 {
648
+ schema = {
649
+ operationId: "get-server-info",
650
+ tags: ["Server"],
651
+ summary: "Get server info"
652
+ };
653
+ async handle(c) {
654
+ const { basicAuth: basicAuth2, ...config } = c.get("config");
655
+ return {
656
+ version: settings.version,
657
+ config,
658
+ auth: c.get("authentication_type") ? {
659
+ type: c.get("authentication_type"),
660
+ username: c.get("authentication_username")
661
+ } : void 0
662
+ };
663
+ }
664
+ };
665
+
666
+ // src/index.ts
667
+ function R2Explorer(config) {
668
+ extendZodWithOpenApi(z13);
669
+ config = config || {};
670
+ if (config.readonly !== false) config.readonly = true;
671
+ const openapiSchema = {
672
+ openapi: "3.1.0",
673
+ info: {
674
+ title: "R2 Explorer API",
675
+ version: settings.version
676
+ }
677
+ };
678
+ if (config.basicAuth) {
679
+ openapiSchema["security"] = [
680
+ {
681
+ basicAuth: []
682
+ }
683
+ ];
684
+ }
685
+ const app = new Hono();
686
+ app.use("*", async (c, next) => {
687
+ c.set("config", config);
688
+ await next();
689
+ });
690
+ const openapi = fromHono(app, {
691
+ schema: openapiSchema,
692
+ raiseUnknownParameters: true,
693
+ generateOperationIds: false
694
+ });
695
+ if (config.cors === true) {
696
+ app.use(
697
+ "*",
698
+ cors({
699
+ origin: "*",
700
+ allowMethods: ["*"],
701
+ credentials: true
702
+ })
703
+ );
704
+ }
705
+ if (config.readonly === true) {
706
+ app.use("*", readOnlyMiddleware);
707
+ }
708
+ if (config.cfAccessTeamName) {
709
+ app.use("*", cloudflareAccess(config.cfAccessTeamName));
710
+ app.use("*", async (c, next) => {
711
+ c.set("authentication_type", "cloudflare-access");
712
+ c.set("authentication_username", c.get("accessPayload").email);
713
+ await next();
714
+ });
715
+ }
716
+ if (config.basicAuth) {
717
+ openapi.registry.registerComponent("securitySchemes", "basicAuth", {
718
+ type: "http",
719
+ scheme: "basic"
720
+ });
721
+ app.use(
722
+ "*",
723
+ basicAuth({
724
+ verifyUser: (username, password, c) => {
725
+ const users = Array.isArray(c.get("config").basicAuth) ? c.get("config").basicAuth : [c.get("config").basicAuth];
726
+ for (const user of users) {
727
+ if (user.username === username && user.password === password) {
728
+ c.set("authentication_type", "basic-auth");
729
+ c.set("authentication_username", username);
730
+ return true;
731
+ }
732
+ }
733
+ return false;
734
+ }
735
+ })
736
+ );
737
+ }
738
+ openapi.get("/api/server/config", GetInfo);
739
+ openapi.get("/api/buckets", ListBuckets);
740
+ openapi.get("/api/buckets/:bucket", ListObjects);
741
+ openapi.post("/api/buckets/:bucket/move", MoveObject);
742
+ openapi.post("/api/buckets/:bucket/folder", CreateFolder);
743
+ openapi.post("/api/buckets/:bucket/upload", PutObject);
744
+ openapi.post("/api/buckets/:bucket/multipart/create", CreateUpload);
745
+ openapi.post("/api/buckets/:bucket/multipart/upload", PartUpload);
746
+ openapi.post("/api/buckets/:bucket/multipart/complete", CompleteUpload);
747
+ openapi.post("/api/buckets/:bucket/delete", DeleteObject);
748
+ openapi.on("head", "/api/buckets/:bucket/:key", HeadObject);
749
+ openapi.get("/api/buckets/:bucket/:key/head", HeadObject);
750
+ openapi.get("/api/buckets/:bucket/:key", GetObject);
751
+ openapi.post("/api/buckets/:bucket/:key", PutMetadata);
752
+ openapi.post("/api/emails/send", SendEmail);
753
+ openapi.get("/", dashboardIndex);
754
+ openapi.get("*", dashboardRedirect);
755
+ app.all(
756
+ "*",
757
+ () => Response.json({ msg: "404, not found!" }, { status: 404 })
758
+ );
759
+ return {
760
+ // TODO: improve event type
761
+ async email(event, env, context) {
762
+ await receiveEmail(event, env, context, config);
763
+ },
764
+ async fetch(request, env, context) {
765
+ return app.fetch(request, env, context);
766
+ }
767
+ };
768
+ }
769
+ export {
770
+ R2Explorer
771
+ };