@edgestore/react 0.0.0-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/dist/contextProvider.d.ts +36 -0
- package/dist/contextProvider.d.ts.map +1 -0
- package/dist/createNextProxy.d.ts +42 -0
- package/dist/createNextProxy.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +222 -0
- package/dist/index.mjs +198 -0
- package/dist/libs/errors/EdgeStoreError.d.ts +5 -0
- package/dist/libs/errors/EdgeStoreError.d.ts.map +1 -0
- package/package.json +73 -0
- package/src/contextProvider.tsx +141 -0
- package/src/createNextProxy.ts +210 -0
- package/src/index.ts +1 -0
- package/src/libs/errors/EdgeStoreError.ts +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
### Next.js Setup
|
|
4
|
+
|
|
5
|
+
#### Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @edgestore/react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
#### Environment Variables
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# .env
|
|
15
|
+
EDGE_STORE_ACCESS_KEY=your-access-key
|
|
16
|
+
EDGE_STORE_SECRET_KEY=your-secret-key
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
#### API Route
|
|
20
|
+
|
|
21
|
+
```jsx
|
|
22
|
+
// pages/api/edgestore/[...edgestore].js
|
|
23
|
+
import EdgeStore from '@edgestore/react/next';
|
|
24
|
+
|
|
25
|
+
export default EdgeStore();
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
#### Provider
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
// pages/_app.jsx
|
|
32
|
+
import { EdgeStoreProvider } from '@edgestore/react';
|
|
33
|
+
|
|
34
|
+
export default function App({ Component, pageProps }) {
|
|
35
|
+
return (
|
|
36
|
+
<EdgeStoreProvider>
|
|
37
|
+
<Component {...pageProps} />
|
|
38
|
+
</EdgeStoreProvider>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Upload image
|
|
44
|
+
|
|
45
|
+
```jsx
|
|
46
|
+
import { useEdgeStore } from '@edgestore/react';
|
|
47
|
+
|
|
48
|
+
const Page = () => {
|
|
49
|
+
const [file, setFile] = useState(null);
|
|
50
|
+
const { upload } = useEdgeStore();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div>
|
|
54
|
+
<input type="file" onChange={(e) => setFile(e.target.files[0])} />
|
|
55
|
+
<button
|
|
56
|
+
onClick={async () => {
|
|
57
|
+
await upload({
|
|
58
|
+
file,
|
|
59
|
+
key: 'path/to/image.jpg',
|
|
60
|
+
});
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
Upload
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default Page;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Show image
|
|
73
|
+
|
|
74
|
+
```jsx
|
|
75
|
+
import { useEdgeStore } from '@edgestore/react';
|
|
76
|
+
|
|
77
|
+
const Page = () => {
|
|
78
|
+
const { getImgSrc } = useEdgeStore();
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div>
|
|
82
|
+
<img src={getImgSrc('path/to/image.jpg')} />
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AnyRouter } from '@edgestore/server/core';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { BucketFunctions } from './createNextProxy';
|
|
4
|
+
type EdgeStoreContextValue<TRouter extends AnyRouter> = {
|
|
5
|
+
edgestore: BucketFunctions<TRouter>;
|
|
6
|
+
/**
|
|
7
|
+
* In development, if this is a protected file, this function will add the token as a query param to the url.
|
|
8
|
+
* This is needed because third party cookies don't work with http urls.
|
|
9
|
+
*/
|
|
10
|
+
getSrc: (url: string) => string;
|
|
11
|
+
};
|
|
12
|
+
export declare function createEdgeStoreProvider<TRouter extends AnyRouter>(opts?: {
|
|
13
|
+
/**
|
|
14
|
+
* The maximum number of concurrent uploads.
|
|
15
|
+
*
|
|
16
|
+
* Uploads will automatically be queued if this limit is reached.
|
|
17
|
+
*
|
|
18
|
+
* @default 5
|
|
19
|
+
*/
|
|
20
|
+
maxConcurrentUploads?: number;
|
|
21
|
+
}): {
|
|
22
|
+
EdgeStoreProvider: ({ children, basePath, }: {
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
/**
|
|
25
|
+
* In case your app is not hosted at the root of your domain, you can specify the base path here.
|
|
26
|
+
* If you set this, make sure to set the full path to the EdgeStore API.
|
|
27
|
+
* e.g. `/my-app/api/edgestore` or `https://example.com/my-app/api/edgestore`
|
|
28
|
+
*
|
|
29
|
+
* @example - If your app is hosted at `https://example.com/my-app`, you can set the `basePath` to `/my-app/api/edgestore`.
|
|
30
|
+
*/
|
|
31
|
+
basePath?: string | undefined;
|
|
32
|
+
}) => JSX.Element;
|
|
33
|
+
useEdgeStore: () => EdgeStoreContextValue<TRouter>;
|
|
34
|
+
};
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=contextProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contextProvider.d.ts","sourceRoot":"","sources":["../src/contextProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAmB,MAAM,mBAAmB,CAAC;AAKrE,KAAK,qBAAqB,CAAC,OAAO,SAAS,SAAS,IAAI;IACtD,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;IACpC;;;OAGG;IACH,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CACjC,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,OAAO,SAAS,SAAS,EAAE,IAAI,CAAC,EAAE;IACxE;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;;kBAUa,MAAM,SAAS;QACzB;;;;;;WAMG;;;;EAgCN"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { AnyRouter, InferBucketPathKeys, InferMetadataObject } from '@edgestore/server/core';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
export type BucketFunctions<TRouter extends AnyRouter> = {
|
|
5
|
+
[K in keyof TRouter['buckets']]: {
|
|
6
|
+
upload: (params: z.infer<TRouter['buckets'][K]['_def']['input']> extends object ? {
|
|
7
|
+
file: File;
|
|
8
|
+
input: z.infer<TRouter['buckets'][K]['_def']['input']>;
|
|
9
|
+
onProgressChange?: OnProgressChangeHandler;
|
|
10
|
+
options?: UploadOptions;
|
|
11
|
+
} : {
|
|
12
|
+
file: File;
|
|
13
|
+
onProgressChange?: OnProgressChangeHandler;
|
|
14
|
+
options?: UploadOptions;
|
|
15
|
+
}) => Promise<{
|
|
16
|
+
url: string;
|
|
17
|
+
thumbnailUrl: TRouter['buckets'][K]['_def']['type'] extends 'IMAGE' ? string | null : never;
|
|
18
|
+
size: number;
|
|
19
|
+
uploadedAt: Date;
|
|
20
|
+
metadata: InferMetadataObject<TRouter['buckets'][K]>;
|
|
21
|
+
path: {
|
|
22
|
+
[TKey in InferBucketPathKeys<TRouter['buckets'][K]>]: string;
|
|
23
|
+
};
|
|
24
|
+
}>;
|
|
25
|
+
delete: (params: {
|
|
26
|
+
url: string;
|
|
27
|
+
}) => Promise<{
|
|
28
|
+
success: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
type OnProgressChangeHandler = (progress: number) => void;
|
|
33
|
+
type UploadOptions = {
|
|
34
|
+
replaceTargetUrl?: string;
|
|
35
|
+
};
|
|
36
|
+
export declare function createNextProxy<TRouter extends AnyRouter>({ apiPath, uploadingCountRef, maxConcurrentUploads, }: {
|
|
37
|
+
apiPath: string;
|
|
38
|
+
uploadingCountRef: React.MutableRefObject<number>;
|
|
39
|
+
maxConcurrentUploads?: number;
|
|
40
|
+
}): BucketFunctions<TRouter>;
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=createNextProxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createNextProxy.d.ts","sourceRoot":"","sources":["../src/createNextProxy.ts"],"names":[],"mappings":";AAAA,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,MAAM,eAAe,CAAC,OAAO,SAAS,SAAS,IAAI;KACtD,CAAC,IAAI,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG;QAC/B,MAAM,EAAE,CACN,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,MAAM,GAClE;YACE,IAAI,EAAE,IAAI,CAAC;YACX,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;YACvD,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;YAC3C,OAAO,CAAC,EAAE,aAAa,CAAC;SACzB,GACD;YACE,IAAI,EAAE,IAAI,CAAC;YACX,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;YAC3C,OAAO,CAAC,EAAE,aAAa,CAAC;SACzB,KACF,OAAO,CAAC;YACX,GAAG,EAAE,MAAM,CAAC;YACZ,YAAY,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,SAAS,OAAO,GAC/D,MAAM,GAAG,IAAI,GACb,KAAK,CAAC;YACV,IAAI,EAAE,MAAM,CAAC;YACb,UAAU,EAAE,IAAI,CAAC;YACjB,QAAQ,EAAE,mBAAmB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACrD,IAAI,EAAE;iBACH,IAAI,IAAI,mBAAmB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM;aAC7D,CAAC;SACH,CAAC,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,KAAK,OAAO,CAAC;YAC3C,OAAO,EAAE,OAAO,CAAC;SAClB,CAAC,CAAC;KACJ;CACF,CAAC;AAEF,KAAK,uBAAuB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAE1D,KAAK,aAAa,GAAG;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,wBAAgB,eAAe,CAAC,OAAO,SAAS,SAAS,EAAE,EACzD,OAAO,EACP,iBAAiB,EACjB,oBAAwB,GACzB,EAAE;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAClD,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,4BAiCA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var React = require('react');
|
|
6
|
+
|
|
7
|
+
function _interopNamespace(e) {
|
|
8
|
+
if (e && e.__esModule) return e;
|
|
9
|
+
var n = Object.create(null);
|
|
10
|
+
if (e) {
|
|
11
|
+
Object.keys(e).forEach(function (k) {
|
|
12
|
+
if (k !== 'default') {
|
|
13
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
14
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: function () { return e[k]; }
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
n["default"] = e;
|
|
22
|
+
return Object.freeze(n);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
26
|
+
|
|
27
|
+
class EdgeStoreError extends Error {
|
|
28
|
+
constructor(message){
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'EdgeStoreError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createNextProxy({ apiPath, uploadingCountRef, maxConcurrentUploads = 5 }) {
|
|
35
|
+
return new Proxy({}, {
|
|
36
|
+
get (_, prop) {
|
|
37
|
+
const bucketName = prop;
|
|
38
|
+
const bucketFunctions = {
|
|
39
|
+
upload: async (params)=>{
|
|
40
|
+
try {
|
|
41
|
+
params.onProgressChange?.(0);
|
|
42
|
+
while(uploadingCountRef.current >= maxConcurrentUploads && uploadingCountRef.current > 0){
|
|
43
|
+
await new Promise((resolve)=>setTimeout(resolve, 300));
|
|
44
|
+
}
|
|
45
|
+
uploadingCountRef.current++;
|
|
46
|
+
return await uploadFile(params, {
|
|
47
|
+
bucketName: bucketName,
|
|
48
|
+
apiPath
|
|
49
|
+
});
|
|
50
|
+
} finally{
|
|
51
|
+
uploadingCountRef.current--;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
delete: async (params)=>{
|
|
55
|
+
return await deleteFile(params, {
|
|
56
|
+
bucketName: bucketName,
|
|
57
|
+
apiPath
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return bucketFunctions;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function uploadFile({ file, input, onProgressChange, options }, { apiPath, bucketName }) {
|
|
66
|
+
try {
|
|
67
|
+
onProgressChange?.(0);
|
|
68
|
+
const res = await fetch(`${apiPath}/request-upload`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
bucketName,
|
|
72
|
+
input,
|
|
73
|
+
fileInfo: {
|
|
74
|
+
extension: file.name.split('.').pop(),
|
|
75
|
+
type: file.type,
|
|
76
|
+
size: file.size,
|
|
77
|
+
replaceTargetUrl: options?.replaceTargetUrl
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/json'
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
const json = await res.json();
|
|
85
|
+
if (!json.uploadUrl) {
|
|
86
|
+
throw new EdgeStoreError('An error occurred');
|
|
87
|
+
}
|
|
88
|
+
// Upload the file to the signed URL and get the progress
|
|
89
|
+
await uploadFileInner(file, json.uploadUrl, onProgressChange);
|
|
90
|
+
return {
|
|
91
|
+
url: json.accessUrl,
|
|
92
|
+
thumbnailUrl: json.thumbnailUrl,
|
|
93
|
+
size: json.size,
|
|
94
|
+
uploadedAt: new Date(json.uploadedAt),
|
|
95
|
+
path: json.path,
|
|
96
|
+
metadata: json.metadata
|
|
97
|
+
};
|
|
98
|
+
} catch (e) {
|
|
99
|
+
onProgressChange?.(0);
|
|
100
|
+
throw e;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const uploadFileInner = async (file, uploadUrl, onProgressChange)=>{
|
|
104
|
+
const promise = new Promise((resolve, reject)=>{
|
|
105
|
+
const request = new XMLHttpRequest();
|
|
106
|
+
request.open('PUT', uploadUrl);
|
|
107
|
+
request.addEventListener('loadstart', ()=>{
|
|
108
|
+
onProgressChange?.(0);
|
|
109
|
+
});
|
|
110
|
+
request.upload.addEventListener('progress', (e)=>{
|
|
111
|
+
if (e.lengthComputable) {
|
|
112
|
+
// 2 decimal progress
|
|
113
|
+
const progress = Math.round(e.loaded / e.total * 10000) / 100;
|
|
114
|
+
onProgressChange?.(progress);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
request.addEventListener('error', ()=>{
|
|
118
|
+
reject(new Error('Error uploading file'));
|
|
119
|
+
});
|
|
120
|
+
request.addEventListener('abort', ()=>{
|
|
121
|
+
reject(new Error('File upload aborted'));
|
|
122
|
+
});
|
|
123
|
+
request.addEventListener('loadend', ()=>{
|
|
124
|
+
resolve();
|
|
125
|
+
});
|
|
126
|
+
request.send(file);
|
|
127
|
+
});
|
|
128
|
+
return promise;
|
|
129
|
+
};
|
|
130
|
+
async function deleteFile({ url }, { apiPath, bucketName }) {
|
|
131
|
+
const res = await fetch(`${apiPath}/delete-file`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
url,
|
|
135
|
+
bucketName
|
|
136
|
+
}),
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json'
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
throw new EdgeStoreError('An error occurred');
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
success: true
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const DEFAULT_BASE_URL = process.env.NEXT_PUBLIC_EDGE_STORE_BASE_URL ?? 'https://files.edge-store.com';
|
|
150
|
+
function createEdgeStoreProvider(opts) {
|
|
151
|
+
const EdgeStoreContext = /*#__PURE__*/ React__namespace.createContext(undefined);
|
|
152
|
+
const EdgeStoreProvider = ({ // TODO: Add basePath when custom domain is supported
|
|
153
|
+
children, basePath })=>{
|
|
154
|
+
return EdgeStoreProviderInner({
|
|
155
|
+
children,
|
|
156
|
+
context: EdgeStoreContext,
|
|
157
|
+
basePath,
|
|
158
|
+
maxConcurrentUploads: opts?.maxConcurrentUploads
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
function useEdgeStore() {
|
|
162
|
+
if (!EdgeStoreContext) {
|
|
163
|
+
throw new Error('React Context is unavailable in Server Components');
|
|
164
|
+
}
|
|
165
|
+
// @ts-expect-error - We know that the context value should not be undefined
|
|
166
|
+
const value = React__namespace.useContext(EdgeStoreContext);
|
|
167
|
+
if (!value && process.env.NODE_ENV !== 'production') {
|
|
168
|
+
throw new Error('[edge-store]: `useEdgeStore` must be wrapped in a <EdgeStoreProvider />');
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
EdgeStoreProvider,
|
|
174
|
+
useEdgeStore
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function EdgeStoreProviderInner({ children, context, basePath, maxConcurrentUploads }) {
|
|
178
|
+
const apiPath = basePath ? `${basePath}` : '/api/edgestore';
|
|
179
|
+
const [token, setToken] = React__namespace.useState(null);
|
|
180
|
+
const uploadingCountRef = React__namespace.useRef(0);
|
|
181
|
+
React__namespace.useEffect(()=>{
|
|
182
|
+
void fetch(`${apiPath}/init`, {
|
|
183
|
+
method: 'POST'
|
|
184
|
+
}).then(async (res)=>{
|
|
185
|
+
if (res.ok) {
|
|
186
|
+
const json = await res.json();
|
|
187
|
+
setToken(json.token);
|
|
188
|
+
await fetch(`${DEFAULT_BASE_URL}/_init`, {
|
|
189
|
+
method: 'GET',
|
|
190
|
+
headers: {
|
|
191
|
+
'x-edgestore-token': json.token
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}, []);
|
|
197
|
+
function getSrc(url) {
|
|
198
|
+
if (// in production we use cookies, so we don't need a token
|
|
199
|
+
process.env.NODE_ENV === 'production' || // public urls don't need a token
|
|
200
|
+
// e.g. https://files.edge-store.com/project/bucket/_public/...
|
|
201
|
+
url.match(/^https?:\/\/[^\/]+\/[^\/]+\/[^\/]+\/_public\/.+/)) {
|
|
202
|
+
return `${url}`;
|
|
203
|
+
} else {
|
|
204
|
+
// in development, third party cookies don't work, so we need to pass the token as a query param
|
|
205
|
+
const uri = new URL(url);
|
|
206
|
+
uri.searchParams.set('token', token ?? '');
|
|
207
|
+
return `${uri}`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return /*#__PURE__*/ React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/ React__namespace.createElement(context.Provider, {
|
|
211
|
+
value: {
|
|
212
|
+
edgestore: createNextProxy({
|
|
213
|
+
apiPath,
|
|
214
|
+
uploadingCountRef,
|
|
215
|
+
maxConcurrentUploads
|
|
216
|
+
}),
|
|
217
|
+
getSrc
|
|
218
|
+
}
|
|
219
|
+
}, children));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
exports.createEdgeStoreProvider = createEdgeStoreProvider;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
class EdgeStoreError extends Error {
|
|
4
|
+
constructor(message){
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'EdgeStoreError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function createNextProxy({ apiPath, uploadingCountRef, maxConcurrentUploads = 5 }) {
|
|
11
|
+
return new Proxy({}, {
|
|
12
|
+
get (_, prop) {
|
|
13
|
+
const bucketName = prop;
|
|
14
|
+
const bucketFunctions = {
|
|
15
|
+
upload: async (params)=>{
|
|
16
|
+
try {
|
|
17
|
+
params.onProgressChange?.(0);
|
|
18
|
+
while(uploadingCountRef.current >= maxConcurrentUploads && uploadingCountRef.current > 0){
|
|
19
|
+
await new Promise((resolve)=>setTimeout(resolve, 300));
|
|
20
|
+
}
|
|
21
|
+
uploadingCountRef.current++;
|
|
22
|
+
return await uploadFile(params, {
|
|
23
|
+
bucketName: bucketName,
|
|
24
|
+
apiPath
|
|
25
|
+
});
|
|
26
|
+
} finally{
|
|
27
|
+
uploadingCountRef.current--;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
delete: async (params)=>{
|
|
31
|
+
return await deleteFile(params, {
|
|
32
|
+
bucketName: bucketName,
|
|
33
|
+
apiPath
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
return bucketFunctions;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async function uploadFile({ file, input, onProgressChange, options }, { apiPath, bucketName }) {
|
|
42
|
+
try {
|
|
43
|
+
onProgressChange?.(0);
|
|
44
|
+
const res = await fetch(`${apiPath}/request-upload`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
bucketName,
|
|
48
|
+
input,
|
|
49
|
+
fileInfo: {
|
|
50
|
+
extension: file.name.split('.').pop(),
|
|
51
|
+
type: file.type,
|
|
52
|
+
size: file.size,
|
|
53
|
+
replaceTargetUrl: options?.replaceTargetUrl
|
|
54
|
+
}
|
|
55
|
+
}),
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'application/json'
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
const json = await res.json();
|
|
61
|
+
if (!json.uploadUrl) {
|
|
62
|
+
throw new EdgeStoreError('An error occurred');
|
|
63
|
+
}
|
|
64
|
+
// Upload the file to the signed URL and get the progress
|
|
65
|
+
await uploadFileInner(file, json.uploadUrl, onProgressChange);
|
|
66
|
+
return {
|
|
67
|
+
url: json.accessUrl,
|
|
68
|
+
thumbnailUrl: json.thumbnailUrl,
|
|
69
|
+
size: json.size,
|
|
70
|
+
uploadedAt: new Date(json.uploadedAt),
|
|
71
|
+
path: json.path,
|
|
72
|
+
metadata: json.metadata
|
|
73
|
+
};
|
|
74
|
+
} catch (e) {
|
|
75
|
+
onProgressChange?.(0);
|
|
76
|
+
throw e;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const uploadFileInner = async (file, uploadUrl, onProgressChange)=>{
|
|
80
|
+
const promise = new Promise((resolve, reject)=>{
|
|
81
|
+
const request = new XMLHttpRequest();
|
|
82
|
+
request.open('PUT', uploadUrl);
|
|
83
|
+
request.addEventListener('loadstart', ()=>{
|
|
84
|
+
onProgressChange?.(0);
|
|
85
|
+
});
|
|
86
|
+
request.upload.addEventListener('progress', (e)=>{
|
|
87
|
+
if (e.lengthComputable) {
|
|
88
|
+
// 2 decimal progress
|
|
89
|
+
const progress = Math.round(e.loaded / e.total * 10000) / 100;
|
|
90
|
+
onProgressChange?.(progress);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
request.addEventListener('error', ()=>{
|
|
94
|
+
reject(new Error('Error uploading file'));
|
|
95
|
+
});
|
|
96
|
+
request.addEventListener('abort', ()=>{
|
|
97
|
+
reject(new Error('File upload aborted'));
|
|
98
|
+
});
|
|
99
|
+
request.addEventListener('loadend', ()=>{
|
|
100
|
+
resolve();
|
|
101
|
+
});
|
|
102
|
+
request.send(file);
|
|
103
|
+
});
|
|
104
|
+
return promise;
|
|
105
|
+
};
|
|
106
|
+
async function deleteFile({ url }, { apiPath, bucketName }) {
|
|
107
|
+
const res = await fetch(`${apiPath}/delete-file`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
url,
|
|
111
|
+
bucketName
|
|
112
|
+
}),
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json'
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
throw new EdgeStoreError('An error occurred');
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
success: true
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const DEFAULT_BASE_URL = process.env.NEXT_PUBLIC_EDGE_STORE_BASE_URL ?? 'https://files.edge-store.com';
|
|
126
|
+
function createEdgeStoreProvider(opts) {
|
|
127
|
+
const EdgeStoreContext = /*#__PURE__*/ React.createContext(undefined);
|
|
128
|
+
const EdgeStoreProvider = ({ // TODO: Add basePath when custom domain is supported
|
|
129
|
+
children, basePath })=>{
|
|
130
|
+
return EdgeStoreProviderInner({
|
|
131
|
+
children,
|
|
132
|
+
context: EdgeStoreContext,
|
|
133
|
+
basePath,
|
|
134
|
+
maxConcurrentUploads: opts?.maxConcurrentUploads
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
function useEdgeStore() {
|
|
138
|
+
if (!EdgeStoreContext) {
|
|
139
|
+
throw new Error('React Context is unavailable in Server Components');
|
|
140
|
+
}
|
|
141
|
+
// @ts-expect-error - We know that the context value should not be undefined
|
|
142
|
+
const value = React.useContext(EdgeStoreContext);
|
|
143
|
+
if (!value && process.env.NODE_ENV !== 'production') {
|
|
144
|
+
throw new Error('[edge-store]: `useEdgeStore` must be wrapped in a <EdgeStoreProvider />');
|
|
145
|
+
}
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
EdgeStoreProvider,
|
|
150
|
+
useEdgeStore
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function EdgeStoreProviderInner({ children, context, basePath, maxConcurrentUploads }) {
|
|
154
|
+
const apiPath = basePath ? `${basePath}` : '/api/edgestore';
|
|
155
|
+
const [token, setToken] = React.useState(null);
|
|
156
|
+
const uploadingCountRef = React.useRef(0);
|
|
157
|
+
React.useEffect(()=>{
|
|
158
|
+
void fetch(`${apiPath}/init`, {
|
|
159
|
+
method: 'POST'
|
|
160
|
+
}).then(async (res)=>{
|
|
161
|
+
if (res.ok) {
|
|
162
|
+
const json = await res.json();
|
|
163
|
+
setToken(json.token);
|
|
164
|
+
await fetch(`${DEFAULT_BASE_URL}/_init`, {
|
|
165
|
+
method: 'GET',
|
|
166
|
+
headers: {
|
|
167
|
+
'x-edgestore-token': json.token
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}, []);
|
|
173
|
+
function getSrc(url) {
|
|
174
|
+
if (// in production we use cookies, so we don't need a token
|
|
175
|
+
process.env.NODE_ENV === 'production' || // public urls don't need a token
|
|
176
|
+
// e.g. https://files.edge-store.com/project/bucket/_public/...
|
|
177
|
+
url.match(/^https?:\/\/[^\/]+\/[^\/]+\/[^\/]+\/_public\/.+/)) {
|
|
178
|
+
return `${url}`;
|
|
179
|
+
} else {
|
|
180
|
+
// in development, third party cookies don't work, so we need to pass the token as a query param
|
|
181
|
+
const uri = new URL(url);
|
|
182
|
+
uri.searchParams.set('token', token ?? '');
|
|
183
|
+
return `${uri}`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(context.Provider, {
|
|
187
|
+
value: {
|
|
188
|
+
edgestore: createNextProxy({
|
|
189
|
+
apiPath,
|
|
190
|
+
uploadingCountRef,
|
|
191
|
+
maxConcurrentUploads
|
|
192
|
+
}),
|
|
193
|
+
getSrc
|
|
194
|
+
}
|
|
195
|
+
}, children));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export { createEdgeStoreProvider };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EdgeStoreError.d.ts","sourceRoot":"","sources":["../../../src/libs/errors/EdgeStoreError.ts"],"names":[],"mappings":"AAAA,cAAM,cAAe,SAAQ,KAAK;gBACpB,OAAO,EAAE,MAAM;CAI5B;AAED,eAAe,cAAc,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@edgestore/react",
|
|
3
|
+
"version": "0.0.0-alpha.12",
|
|
4
|
+
"description": "Image Handling for React/Next.js",
|
|
5
|
+
"homepage": "https://edge-store.com",
|
|
6
|
+
"repository": "https://github.com/edgestorejs/edge-store.git",
|
|
7
|
+
"author": "Ravi <me@ravi.com>",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"module": "dist/index.mjs",
|
|
10
|
+
"typings": "dist/index.d.ts",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"react",
|
|
13
|
+
"nodejs",
|
|
14
|
+
"nextjs",
|
|
15
|
+
"image",
|
|
16
|
+
"cdn",
|
|
17
|
+
"edgestore",
|
|
18
|
+
"edge-store"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "rollup --config rollup.config.ts --configPlugin rollup-plugin-swc3",
|
|
22
|
+
"dev": "pnpm build --watch",
|
|
23
|
+
"codegen:entrypoints": "tsx entrypoints.script.ts",
|
|
24
|
+
"lint": "eslint --cache --ext \".js,.ts,.tsx\" --ignore-path ../../.gitignore --report-unused-disable-directives src"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
"./package.json": "./package.json",
|
|
28
|
+
".": {
|
|
29
|
+
"import": "./dist/index.mjs",
|
|
30
|
+
"require": "./dist/index.js",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src",
|
|
37
|
+
"README.md",
|
|
38
|
+
"package.json",
|
|
39
|
+
"!**/*.test.*"
|
|
40
|
+
],
|
|
41
|
+
"private": false,
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@aws-sdk/client-s3": "^3.294.0",
|
|
48
|
+
"@aws-sdk/s3-request-presigner": "^3.294.0",
|
|
49
|
+
"@panva/hkdf": "^1.0.4",
|
|
50
|
+
"cookie": "^0.5.0",
|
|
51
|
+
"jose": "^4.13.1",
|
|
52
|
+
"uuid": "^9.0.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@edgestore/server": "0.0.0-alpha.12",
|
|
56
|
+
"next": "*",
|
|
57
|
+
"react": ">=16.8.0",
|
|
58
|
+
"react-dom": ">=16.8.0",
|
|
59
|
+
"zod": ">=3.0.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@edgestore/server": "0.0.0-alpha.12",
|
|
63
|
+
"@types/cookie": "^0.5.1",
|
|
64
|
+
"@types/node": "^18.11.18",
|
|
65
|
+
"@types/uuid": "^9.0.1",
|
|
66
|
+
"next": "^13.4.9",
|
|
67
|
+
"react": "^18.2.0",
|
|
68
|
+
"react-dom": "^18.2.0",
|
|
69
|
+
"typescript": "^5.1.6",
|
|
70
|
+
"zod": "^3.21.4"
|
|
71
|
+
},
|
|
72
|
+
"gitHead": "bec47f77e223a231f5e04070aa8da6a609838e6b"
|
|
73
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { AnyRouter } from '@edgestore/server/core';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { BucketFunctions, createNextProxy } from './createNextProxy';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_BASE_URL =
|
|
6
|
+
process.env.NEXT_PUBLIC_EDGE_STORE_BASE_URL ?? 'https://files.edge-store.com';
|
|
7
|
+
|
|
8
|
+
type EdgeStoreContextValue<TRouter extends AnyRouter> = {
|
|
9
|
+
edgestore: BucketFunctions<TRouter>;
|
|
10
|
+
/**
|
|
11
|
+
* In development, if this is a protected file, this function will add the token as a query param to the url.
|
|
12
|
+
* This is needed because third party cookies don't work with http urls.
|
|
13
|
+
*/
|
|
14
|
+
getSrc: (url: string) => string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function createEdgeStoreProvider<TRouter extends AnyRouter>(opts?: {
|
|
18
|
+
/**
|
|
19
|
+
* The maximum number of concurrent uploads.
|
|
20
|
+
*
|
|
21
|
+
* Uploads will automatically be queued if this limit is reached.
|
|
22
|
+
*
|
|
23
|
+
* @default 5
|
|
24
|
+
*/
|
|
25
|
+
maxConcurrentUploads?: number;
|
|
26
|
+
}) {
|
|
27
|
+
const EdgeStoreContext = React.createContext<
|
|
28
|
+
EdgeStoreContextValue<TRouter> | undefined
|
|
29
|
+
>(undefined);
|
|
30
|
+
|
|
31
|
+
const EdgeStoreProvider = ({
|
|
32
|
+
// TODO: Add basePath when custom domain is supported
|
|
33
|
+
children,
|
|
34
|
+
basePath,
|
|
35
|
+
}: {
|
|
36
|
+
children: React.ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* In case your app is not hosted at the root of your domain, you can specify the base path here.
|
|
39
|
+
* If you set this, make sure to set the full path to the EdgeStore API.
|
|
40
|
+
* e.g. `/my-app/api/edgestore` or `https://example.com/my-app/api/edgestore`
|
|
41
|
+
*
|
|
42
|
+
* @example - If your app is hosted at `https://example.com/my-app`, you can set the `basePath` to `/my-app/api/edgestore`.
|
|
43
|
+
*/
|
|
44
|
+
basePath?: string;
|
|
45
|
+
}) => {
|
|
46
|
+
return EdgeStoreProviderInner<TRouter>({
|
|
47
|
+
children,
|
|
48
|
+
context: EdgeStoreContext,
|
|
49
|
+
basePath,
|
|
50
|
+
maxConcurrentUploads: opts?.maxConcurrentUploads,
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function useEdgeStore() {
|
|
55
|
+
if (!EdgeStoreContext) {
|
|
56
|
+
throw new Error('React Context is unavailable in Server Components');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// @ts-expect-error - We know that the context value should not be undefined
|
|
60
|
+
const value: EdgeStoreContextValue<TRouter> =
|
|
61
|
+
React.useContext(EdgeStoreContext);
|
|
62
|
+
if (!value && process.env.NODE_ENV !== 'production') {
|
|
63
|
+
throw new Error(
|
|
64
|
+
'[edge-store]: `useEdgeStore` must be wrapped in a <EdgeStoreProvider />',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
EdgeStoreProvider,
|
|
73
|
+
useEdgeStore,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function EdgeStoreProviderInner<TRouter extends AnyRouter>({
|
|
78
|
+
children,
|
|
79
|
+
context,
|
|
80
|
+
basePath,
|
|
81
|
+
maxConcurrentUploads,
|
|
82
|
+
}: {
|
|
83
|
+
children: React.ReactNode;
|
|
84
|
+
context: React.Context<EdgeStoreContextValue<TRouter> | undefined>;
|
|
85
|
+
basePath?: string;
|
|
86
|
+
maxConcurrentUploads?: number;
|
|
87
|
+
}) {
|
|
88
|
+
const apiPath = basePath ? `${basePath}` : '/api/edgestore';
|
|
89
|
+
const [token, setToken] = React.useState<string | null>(null);
|
|
90
|
+
const uploadingCountRef = React.useRef(0);
|
|
91
|
+
React.useEffect(() => {
|
|
92
|
+
void fetch(`${apiPath}/init`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
}).then(async (res) => {
|
|
95
|
+
if (res.ok) {
|
|
96
|
+
const json = await res.json();
|
|
97
|
+
setToken(json.token);
|
|
98
|
+
await fetch(`${DEFAULT_BASE_URL}/_init`, {
|
|
99
|
+
method: 'GET',
|
|
100
|
+
headers: {
|
|
101
|
+
'x-edgestore-token': json.token,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
function getSrc(url: string) {
|
|
109
|
+
if (
|
|
110
|
+
// in production we use cookies, so we don't need a token
|
|
111
|
+
process.env.NODE_ENV === 'production' ||
|
|
112
|
+
// public urls don't need a token
|
|
113
|
+
// e.g. https://files.edge-store.com/project/bucket/_public/...
|
|
114
|
+
url.match(/^https?:\/\/[^\/]+\/[^\/]+\/[^\/]+\/_public\/.+/)
|
|
115
|
+
) {
|
|
116
|
+
return `${url}`;
|
|
117
|
+
} else {
|
|
118
|
+
// in development, third party cookies don't work, so we need to pass the token as a query param
|
|
119
|
+
const uri = new URL(url);
|
|
120
|
+
uri.searchParams.set('token', token ?? '');
|
|
121
|
+
return `${uri}`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<>
|
|
127
|
+
<context.Provider
|
|
128
|
+
value={{
|
|
129
|
+
edgestore: createNextProxy<TRouter>({
|
|
130
|
+
apiPath,
|
|
131
|
+
uploadingCountRef,
|
|
132
|
+
maxConcurrentUploads,
|
|
133
|
+
}),
|
|
134
|
+
getSrc,
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
{children}
|
|
138
|
+
</context.Provider>
|
|
139
|
+
</>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnyRouter,
|
|
3
|
+
InferBucketPathKeys,
|
|
4
|
+
InferMetadataObject,
|
|
5
|
+
} from '@edgestore/server/core';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import EdgeStoreError from './libs/errors/EdgeStoreError';
|
|
8
|
+
|
|
9
|
+
export type BucketFunctions<TRouter extends AnyRouter> = {
|
|
10
|
+
[K in keyof TRouter['buckets']]: {
|
|
11
|
+
upload: (
|
|
12
|
+
params: z.infer<TRouter['buckets'][K]['_def']['input']> extends object
|
|
13
|
+
? {
|
|
14
|
+
file: File;
|
|
15
|
+
input: z.infer<TRouter['buckets'][K]['_def']['input']>;
|
|
16
|
+
onProgressChange?: OnProgressChangeHandler;
|
|
17
|
+
options?: UploadOptions;
|
|
18
|
+
}
|
|
19
|
+
: {
|
|
20
|
+
file: File;
|
|
21
|
+
onProgressChange?: OnProgressChangeHandler;
|
|
22
|
+
options?: UploadOptions;
|
|
23
|
+
},
|
|
24
|
+
) => Promise<{
|
|
25
|
+
url: string;
|
|
26
|
+
thumbnailUrl: TRouter['buckets'][K]['_def']['type'] extends 'IMAGE'
|
|
27
|
+
? string | null
|
|
28
|
+
: never;
|
|
29
|
+
size: number;
|
|
30
|
+
uploadedAt: Date;
|
|
31
|
+
metadata: InferMetadataObject<TRouter['buckets'][K]>;
|
|
32
|
+
path: {
|
|
33
|
+
[TKey in InferBucketPathKeys<TRouter['buckets'][K]>]: string;
|
|
34
|
+
};
|
|
35
|
+
}>;
|
|
36
|
+
delete: (params: { url: string }) => Promise<{
|
|
37
|
+
success: boolean;
|
|
38
|
+
}>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type OnProgressChangeHandler = (progress: number) => void;
|
|
43
|
+
|
|
44
|
+
type UploadOptions = {
|
|
45
|
+
replaceTargetUrl?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function createNextProxy<TRouter extends AnyRouter>({
|
|
49
|
+
apiPath,
|
|
50
|
+
uploadingCountRef,
|
|
51
|
+
maxConcurrentUploads = 5,
|
|
52
|
+
}: {
|
|
53
|
+
apiPath: string;
|
|
54
|
+
uploadingCountRef: React.MutableRefObject<number>;
|
|
55
|
+
maxConcurrentUploads?: number;
|
|
56
|
+
}) {
|
|
57
|
+
return new Proxy<BucketFunctions<TRouter>>({} as BucketFunctions<TRouter>, {
|
|
58
|
+
get(_, prop) {
|
|
59
|
+
const bucketName = prop as keyof TRouter['buckets'];
|
|
60
|
+
const bucketFunctions: BucketFunctions<TRouter>[string] = {
|
|
61
|
+
upload: async (params) => {
|
|
62
|
+
try {
|
|
63
|
+
params.onProgressChange?.(0);
|
|
64
|
+
while (
|
|
65
|
+
uploadingCountRef.current >= maxConcurrentUploads &&
|
|
66
|
+
uploadingCountRef.current > 0
|
|
67
|
+
) {
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
69
|
+
}
|
|
70
|
+
uploadingCountRef.current++;
|
|
71
|
+
return await uploadFile(params, {
|
|
72
|
+
bucketName: bucketName as string,
|
|
73
|
+
apiPath,
|
|
74
|
+
});
|
|
75
|
+
} finally {
|
|
76
|
+
uploadingCountRef.current--;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
delete: async (params: { url: string }) => {
|
|
80
|
+
return await deleteFile(params, {
|
|
81
|
+
bucketName: bucketName as string,
|
|
82
|
+
apiPath,
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
return bucketFunctions;
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function uploadFile(
|
|
92
|
+
{
|
|
93
|
+
file,
|
|
94
|
+
input,
|
|
95
|
+
onProgressChange,
|
|
96
|
+
options,
|
|
97
|
+
}: {
|
|
98
|
+
file: File;
|
|
99
|
+
input?: object;
|
|
100
|
+
onProgressChange?: OnProgressChangeHandler;
|
|
101
|
+
options?: UploadOptions;
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
apiPath,
|
|
105
|
+
bucketName,
|
|
106
|
+
}: {
|
|
107
|
+
apiPath: string;
|
|
108
|
+
bucketName: string;
|
|
109
|
+
},
|
|
110
|
+
) {
|
|
111
|
+
try {
|
|
112
|
+
onProgressChange?.(0);
|
|
113
|
+
const res = await fetch(`${apiPath}/request-upload`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
bucketName,
|
|
117
|
+
input,
|
|
118
|
+
fileInfo: {
|
|
119
|
+
extension: file.name.split('.').pop(),
|
|
120
|
+
type: file.type,
|
|
121
|
+
size: file.size,
|
|
122
|
+
replaceTargetUrl: options?.replaceTargetUrl,
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
headers: {
|
|
126
|
+
'Content-Type': 'application/json',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
const json = await res.json();
|
|
130
|
+
if (!json.uploadUrl) {
|
|
131
|
+
throw new EdgeStoreError('An error occurred');
|
|
132
|
+
}
|
|
133
|
+
// Upload the file to the signed URL and get the progress
|
|
134
|
+
await uploadFileInner(file, json.uploadUrl, onProgressChange);
|
|
135
|
+
return {
|
|
136
|
+
url: json.accessUrl,
|
|
137
|
+
thumbnailUrl: json.thumbnailUrl,
|
|
138
|
+
size: json.size,
|
|
139
|
+
uploadedAt: new Date(json.uploadedAt),
|
|
140
|
+
path: json.path,
|
|
141
|
+
metadata: json.metadata,
|
|
142
|
+
};
|
|
143
|
+
} catch (e) {
|
|
144
|
+
onProgressChange?.(0);
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const uploadFileInner = async (
|
|
150
|
+
file: File,
|
|
151
|
+
uploadUrl: string,
|
|
152
|
+
onProgressChange?: OnProgressChangeHandler,
|
|
153
|
+
) => {
|
|
154
|
+
const promise = new Promise<void>((resolve, reject) => {
|
|
155
|
+
const request = new XMLHttpRequest();
|
|
156
|
+
request.open('PUT', uploadUrl);
|
|
157
|
+
request.addEventListener('loadstart', () => {
|
|
158
|
+
onProgressChange?.(0);
|
|
159
|
+
});
|
|
160
|
+
request.upload.addEventListener('progress', (e) => {
|
|
161
|
+
if (e.lengthComputable) {
|
|
162
|
+
// 2 decimal progress
|
|
163
|
+
const progress = Math.round((e.loaded / e.total) * 10000) / 100;
|
|
164
|
+
onProgressChange?.(progress);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
request.addEventListener('error', () => {
|
|
168
|
+
reject(new Error('Error uploading file'));
|
|
169
|
+
});
|
|
170
|
+
request.addEventListener('abort', () => {
|
|
171
|
+
reject(new Error('File upload aborted'));
|
|
172
|
+
});
|
|
173
|
+
request.addEventListener('loadend', () => {
|
|
174
|
+
resolve();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
request.send(file);
|
|
178
|
+
});
|
|
179
|
+
return promise;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
async function deleteFile(
|
|
183
|
+
{
|
|
184
|
+
url,
|
|
185
|
+
}: {
|
|
186
|
+
url: string;
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
apiPath,
|
|
190
|
+
bucketName,
|
|
191
|
+
}: {
|
|
192
|
+
apiPath: string;
|
|
193
|
+
bucketName: string;
|
|
194
|
+
},
|
|
195
|
+
) {
|
|
196
|
+
const res = await fetch(`${apiPath}/delete-file`, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
body: JSON.stringify({
|
|
199
|
+
url,
|
|
200
|
+
bucketName,
|
|
201
|
+
}),
|
|
202
|
+
headers: {
|
|
203
|
+
'Content-Type': 'application/json',
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
if (!res.ok) {
|
|
207
|
+
throw new EdgeStoreError('An error occurred');
|
|
208
|
+
}
|
|
209
|
+
return { success: true };
|
|
210
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './contextProvider';
|