-
Notifications
You must be signed in to change notification settings - Fork 135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add uploadthing driver #390
base: main
Are you sure you want to change the base?
Changes from all commits
4cb6acf
cac6a06
f9a5a15
8d46dab
6444a42
e98edab
4d55f54
4c9a188
a54c648
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# UploadThing | ||
|
||
Store data using UploadThing. | ||
|
||
::note{to="https://uploadthing.com/"} | ||
Learn more about UploadThing. | ||
:: | ||
|
||
```js | ||
import { createStorage } from "unstorage"; | ||
import uploadthingDriver from "unstorage/drivers/uploadthing"; | ||
|
||
const storage = createStorage({ | ||
driver: uploadthingDriver({ | ||
// apiKey: "<your api key>", | ||
}), | ||
}); | ||
``` | ||
|
||
To use, you will need to install `uploadthing` dependency in your project: | ||
|
||
```json | ||
{ | ||
"dependencies": { | ||
"uploadthing": "latest" | ||
} | ||
} | ||
``` | ||
|
||
**Options:** | ||
|
||
- `apiKey`: Your UploadThing API key. Will be automatically inferred from the `UPLOADTHING_SECRET` environment variable if not provided. |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -92,6 +92,7 @@ | |||||
"types-cloudflare-worker": "^1.2.0", | ||||||
"typescript": "^5.3.3", | ||||||
"unbuild": "^2.0.0", | ||||||
"uploadthing": "6.3.2-canary.a35b49f", | ||||||
"vite": "^5.0.11", | ||||||
"vitest": "^1.2.1", | ||||||
"vue": "^3.4.14" | ||||||
|
@@ -108,7 +109,8 @@ | |||||
"@planetscale/database": "^1.13.0", | ||||||
"@upstash/redis": "^1.28.1", | ||||||
"@vercel/kv": "^0.2.4", | ||||||
"idb-keyval": "^6.2.1" | ||||||
"idb-keyval": "^6.2.1", | ||||||
"uploadthing": "^6.0.0" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
}, | ||||||
"peerDependenciesMeta": { | ||||||
"@azure/app-configuration": { | ||||||
|
@@ -146,6 +148,9 @@ | |||||
}, | ||||||
"idb-keyval": { | ||||||
"optional": true | ||||||
}, | ||||||
"uploadthing": { | ||||||
"optional": true | ||||||
} | ||||||
}, | ||||||
"packageManager": "[email protected]" | ||||||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { defineDriver } from "./utils"; | ||
import { ofetch, $Fetch } from "ofetch"; | ||
import { UTApi } from "uploadthing/server"; | ||
|
||
export interface UploadThingOptions { | ||
apiKey: string; | ||
} | ||
|
||
export default defineDriver<UploadThingOptions>((opts) => { | ||
let client: UTApi; | ||
const getClient = () => { | ||
return (client ??= new UTApi({ | ||
apiKey: opts.apiKey, | ||
fetch: ofetch.native, | ||
})); | ||
}; | ||
|
||
async function getKeys() { | ||
const res = await getClient().listFiles({}); | ||
return res.map((file) => file.customId).filter((k): k is string => !!k); | ||
} | ||
|
||
return { | ||
hasItem: async (id) => { | ||
const res = await getClient().getFileUrls(id, { keyType: "customId" }); | ||
return res.length > 0; | ||
}, | ||
getItem: async (id) => { | ||
const url = await getClient() | ||
.getFileUrls(id, { keyType: "customId" }) | ||
.then((res) => { | ||
return res[0]?.url; | ||
}); | ||
if (!url) return null; | ||
return ofetch(url).then((res) => res.text()); | ||
}, | ||
getKeys: () => { | ||
return getKeys(); | ||
}, | ||
setItem: async (key, value, opts) => { | ||
await getClient().uploadFiles( | ||
Object.assign(new Blob([value]), { | ||
name: key, | ||
customId: key, | ||
}), | ||
{ metadata: opts?.metadata } | ||
); | ||
}, | ||
setItems: async (items, opts) => { | ||
await getClient().uploadFiles( | ||
items.map((item) => | ||
Object.assign(new Blob([item.value]), { | ||
name: item.key, | ||
customId: item.key, | ||
}) | ||
), | ||
{ metadata: opts?.metadata } | ||
); | ||
}, | ||
removeItem: async (key, opts) => { | ||
await getClient().deleteFiles([key], { keyType: "customId" }); | ||
}, | ||
clear: async () => { | ||
const keys = await getKeys(); | ||
await getClient().deleteFiles(keys, { keyType: "customId" }); | ||
}, | ||
|
||
// getMeta(key, opts) { | ||
// // TODO: We don't currently have an endpoint to fetch metadata, but it does exist | ||
// }, | ||
}; | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -231,7 +231,7 @@ export function createStorage<T extends StorageValue>( | |
async setItems(items, commonOptions) { | ||
await runBatch(items, commonOptions, async (batch) => { | ||
if (batch.driver.setItems) { | ||
await asyncCall( | ||
return asyncCall( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fairly certain this was a bug before. you called setItems and then setItem for every file so files were getting uploaded twice. noticed this since the newly introduced customids on uploadthing must be unique and we throw if that's not the case and we got duplicate requests when testing the setItems function before i changed the keyword here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch. Do you mind splitting it into a new PR as well? ππΌ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
batch.driver.setItems, | ||
batch.items.map((item) => ({ | ||
key: item.relativeKey, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { afterAll, beforeAll, describe, it } from "vitest"; | ||
import driver from "../../src/drivers/uploadthing"; | ||
import { testDriver } from "./utils"; | ||
import { setupServer } from "msw/node"; | ||
import { rest } from "msw"; | ||
|
||
const store: Record<string, any> = {}; | ||
|
||
const utapiUrl = "https://uploadthing.com/api"; | ||
const utfsUrl = "https://utfs.io/f"; | ||
|
||
const server = setupServer( | ||
rest.post(`${utapiUrl}/getFileUrl`, async (req, res, ctx) => { | ||
const { fileKeys } = await req.json(); | ||
const key = fileKeys[0]; | ||
if (!(key in store)) { | ||
return res(ctx.status(401), ctx.json({ error: "Unauthorized" })); | ||
} | ||
return res( | ||
ctx.status(200), | ||
ctx.json({ | ||
result: { | ||
[key]: `https://utfs.io/f/${key}`, | ||
}, | ||
}) | ||
); | ||
}), | ||
rest.get(`${utfsUrl}/:key`, (req, res, ctx) => { | ||
const key = req.params.key as string; | ||
if (!(key in store)) { | ||
return res(ctx.status(404), ctx.json(null)); | ||
} | ||
return res( | ||
ctx.status(200), | ||
ctx.set("content-type", "application/octet-stream"), | ||
ctx.body(store[key]) | ||
); | ||
}), | ||
rest.post(`${utapiUrl}/uploadFiles`, async (req, res, ctx) => { | ||
console.log("intercepted request"); | ||
return res( | ||
ctx.status(200), | ||
ctx.json({ | ||
data: [ | ||
{ | ||
presignedUrls: [`https://my-s3-server.com/:key`], | ||
}, | ||
], | ||
}) | ||
); | ||
}), | ||
rest.post(`${utapiUrl}/deleteFile`, async (req, res, ctx) => { | ||
console.log("hello????"); | ||
const { fileKeys } = await req.json(); | ||
for (const key of fileKeys) { | ||
delete store[key]; | ||
} | ||
return res(ctx.status(200), ctx.json({ success: true })); | ||
}) | ||
); | ||
|
||
describe( | ||
"drivers: uploadthing", | ||
() => { | ||
// beforeAll(() => { | ||
// server.listen(); | ||
// }); | ||
// afterAll(() => { | ||
// server.close(); | ||
// }); | ||
|
||
testDriver({ | ||
driver: driver({ | ||
apiKey: "sk_live_xxx", | ||
}), | ||
async additionalTests(ctx) {}, | ||
}); | ||
}, | ||
{ timeout: 30e3 } | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.