diff --git a/server/cloudflare-driver.ts b/server/cloudflare-driver.ts deleted file mode 100644 index 5fe74416..00000000 --- a/server/cloudflare-driver.ts +++ /dev/null @@ -1,194 +0,0 @@ -// Temporary hotfix of https://github.com/unjs/unstorage/blob/4d637a117667ae638a6cac657aac139d88a78027/src/drivers/cloudflare-kv-http.ts#L6 - -import { $fetch } from 'ofetch' -import { defineDriver } from 'unstorage' - -const LOG_TAG = '[unstorage] [cloudflare-http] ' - -interface KVAuthAPIToken { - /** - * API Token generated from the [User Profile 'API Tokens' page](https://dash.cloudflare.com/profile/api-tokens) - * of the Cloudflare console. - * @see https://api.cloudflare.com/#getting-started-requests - */ - apiToken: string -} - -interface KVAuthServiceKey { - /** - * A special Cloudflare API key good for a restricted set of endpoints. - * Always begins with "v1.0-", may vary in length. - * May be used to authenticate in place of `apiToken` or `apiKey` and `email`. - * @see https://api.cloudflare.com/#getting-started-requests - */ - userServiceKey: string -} - -interface KVAuthEmailKey { - /** - * Email address associated with your account. - * Should be used along with `apiKey` to authenticate in place of `apiToken`. - */ - email: string - /** - * API key generated on the "My Account" page of the Cloudflare console. - * Should be used along with `email` to authenticate in place of `apiToken`. - * @see https://api.cloudflare.com/#getting-started-requests - */ - apiKey: string -} - -export type KVHTTPOptions = { - /** - * Cloudflare account ID (required) - */ - accountId: string - /** - * The ID of the KV namespace to target (required) - */ - namespaceId: string - /** - * The URL of the Cloudflare API. - * @default https://api.cloudflare.com - */ - apiURL?: string -} & (KVAuthServiceKey | KVAuthAPIToken | KVAuthEmailKey) - -type CloudflareAuthorizationHeaders = { - 'X-Auth-Email': string - 'X-Auth-Key': string - 'X-Auth-User-Service-Key'?: string - Authorization?: `Bearer ${string}` -} | { - 'X-Auth-Email'?: string - 'X-Auth-Key'?: string - 'X-Auth-User-Service-Key': string - Authorization?: `Bearer ${string}` -} | { - 'X-Auth-Email'?: string - 'X-Auth-Key'?: string - 'X-Auth-User-Service-Key'?: string - Authorization: `Bearer ${string}` -} - -export default defineDriver((opts) => { - if (!opts) - throw new Error('Options must be provided.') - - if (!opts.accountId) - throw new Error(`${LOG_TAG}\`accountId\` is required.`) - - if (!opts.namespaceId) - throw new Error(`${LOG_TAG}\`namespaceId\` is required.`) - - let headers: CloudflareAuthorizationHeaders - - if ('apiToken' in opts) { - headers = { Authorization: `Bearer ${opts.apiToken}` } - } - else if ('userServiceKey' in opts) { - headers = { 'X-Auth-User-Service-Key': opts.userServiceKey } - } - else if (opts.email && opts.apiKey) { - headers = { 'X-Auth-Email': opts.email, 'X-Auth-Key': opts.apiKey } - } - else { - throw new Error( - `${LOG_TAG}One of the \`apiToken\`, \`userServiceKey\`, or a combination of \`email\` and \`apiKey\` is required.`, - ) - } - - const apiURL = opts.apiURL || 'https://api.cloudflare.com' - const baseURL = `${apiURL}/client/v4/accounts/${opts.accountId}/storage/kv/namespaces/${opts.namespaceId}` - const kvFetch = $fetch.create({ baseURL, headers }) - - const hasItem = async (key: string) => { - try { - const res = await kvFetch(`/metadata/${key}`) - return res?.success === true - } - catch (err: any) { - if (!err.response) - throw err - if (err.response.status === 404) - return false - throw err - } - } - - const getItem = async (key: string) => { - try { - // Cloudflare API returns with `content-type: application/octet-stream` - return await kvFetch(`/values/${key}`).then(r => r.text()) - } - catch (err: any) { - if (!err.response) - throw err - if (err.response.status === 404) - return null - throw err - } - } - - const setItem = async (key: string, value: any) => { - return await kvFetch(`/values/${key}`, { method: 'PUT', body: value }) - } - - const removeItem = async (key: string) => { - return await kvFetch(`/values/${key}`, { method: 'DELETE' }) - } - - const getKeys = async (base?: string) => { - const keys: string[] = [] - - const params = new URLSearchParams() - if (base) - params.set('prefix', base) - - const firstPage = await kvFetch('/keys', { params }) - firstPage.result.forEach(({ name }: { name: string }) => keys.push(name)) - - const cursor = firstPage.result_info.cursor - if (cursor) - params.set('cursor', cursor) - - while (params.has('cursor')) { - const pageResult = await kvFetch('/keys', { params: Object.fromEntries(params.entries()) }) - pageResult.result.forEach(({ name }: { name: string }) => keys.push(name)) - const pageCursor = pageResult.result_info.cursor - if (pageCursor) - params.set('cursor', pageCursor) - - else - params.delete('cursor') - } - return keys - } - - const clear = async () => { - const keys: string[] = await getKeys() - // Split into chunks of 10000, as the API only allows for 10,000 keys at a time - const chunks = keys.reduce((acc, key, i) => { - if (i % 10000 === 0) - acc.push([]) - acc[acc.length - 1].push(key) - return acc - }, [[]] as string[][]) - // Call bulk delete endpoint with each chunk - await Promise.all(chunks.map((chunk) => { - return kvFetch('/bulk', { - method: 'DELETE', - body: { keys: chunk }, - }) - })) - } - - return { - hasItem, - getItem, - setItem, - removeItem, - getKeys, - clear, - } -}) diff --git a/server/utils/shared.ts b/server/utils/shared.ts index 6b1347bf..3f729893 100644 --- a/server/utils/shared.ts +++ b/server/utils/shared.ts @@ -1,5 +1,6 @@ import fs from 'unstorage/drivers/fs' import memory from 'unstorage/drivers/memory' +import kv from 'unstorage/drivers/cloudflare-kv-http' import { stringifyQuery } from 'ufo' @@ -7,7 +8,6 @@ import { $fetch } from 'ofetch' import type { Storage } from 'unstorage' import cached from '../cache-driver' -import kv from '../cloudflare-driver' // @ts-expect-error virtual import import { env } from '#build-info'