mirror of
https://github.com/unjs/unstorage.git
synced 2026-04-17 03:00:46 -04:00
style: format and lint code
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"extends": [
|
||||
"eslint-config-unjs"
|
||||
],
|
||||
"extends": ["eslint-config-unjs"],
|
||||
"rules": {
|
||||
"unicorn/no-null": 0,
|
||||
"unicorn/prevent-abbreviations": 0
|
||||
"unicorn/prevent-abbreviations": 0,
|
||||
"@typescript-eslint/no-non-null-assertion": 0
|
||||
}
|
||||
}
|
||||
|
||||
0
.prettierrc
Normal file
0
.prettierrc
Normal file
68
demo/App.vue
68
demo/App.vue
@@ -4,64 +4,76 @@
|
||||
<FileTree @open="openTab" />
|
||||
<div class="main">
|
||||
<div class="tabs">
|
||||
<div v-for="tab in state.tabs" :key="tab.path" class="tab" :class="{active: state.path === tab.path }">
|
||||
<span @click="openTab(tab.path)">{{ tab.name }} </span><span @click="closeTab(tab.path)">(x)</span>
|
||||
<div
|
||||
v-for="tab in state.tabs"
|
||||
:key="tab.path"
|
||||
class="tab"
|
||||
:class="{ active: state.path === tab.path }"
|
||||
>
|
||||
<span @click="openTab(tab.path)">{{ tab.name }} </span
|
||||
><span @click="closeTab(tab.path)">(x)</span>
|
||||
</div>
|
||||
</div>
|
||||
<Editor class="editor" :value="state.source" :language="state.language" />
|
||||
<Editor
|
||||
class="editor"
|
||||
:value="state.source"
|
||||
:language="state.language"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, reactive, inject } from 'vue'
|
||||
import Editor from './Editor.vue'
|
||||
import FileTree from './FileTree.vue'
|
||||
import { defineComponent, reactive, inject } from "vue";
|
||||
import Editor from "./Editor.vue";
|
||||
import FileTree from "./FileTree.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Editor,
|
||||
FileTree
|
||||
FileTree,
|
||||
},
|
||||
setup () {
|
||||
const storage = inject('storage')
|
||||
setup() {
|
||||
const storage = inject("storage");
|
||||
const state = reactive({
|
||||
tabs: [],
|
||||
path: undefined,
|
||||
source: '',
|
||||
language: 'javascript'
|
||||
})
|
||||
source: "",
|
||||
language: "javascript",
|
||||
});
|
||||
|
||||
const openTab = async (path) => {
|
||||
const tab = state.tabs.find(tab => tab.path === path)
|
||||
const tab = state.tabs.find((tab) => tab.path === path);
|
||||
if (!tab) {
|
||||
state.tabs.push({
|
||||
name: path.split(':').pop(),
|
||||
path
|
||||
})
|
||||
name: path.split(":").pop(),
|
||||
path,
|
||||
});
|
||||
}
|
||||
const source = await storage.getItem(path)
|
||||
state.language = path.split(':').pop().split('.').pop() || 'javascript'
|
||||
state.path = path
|
||||
state.source = source
|
||||
}
|
||||
const source = await storage.getItem(path);
|
||||
state.language = path.split(":").pop().split(".").pop() || "javascript";
|
||||
state.path = path;
|
||||
state.source = source;
|
||||
};
|
||||
|
||||
const closeTab = (path) => {
|
||||
state.tabs = state.tabs.filter(t => t.path !== path)
|
||||
}
|
||||
state.tabs = state.tabs.filter((t) => t.path !== path);
|
||||
};
|
||||
|
||||
return {
|
||||
state,
|
||||
openTab,
|
||||
closeTab
|
||||
}
|
||||
}
|
||||
})
|
||||
closeTab,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body, html, #app {
|
||||
body,
|
||||
html,
|
||||
#app {
|
||||
margin: 0;
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
@@ -3,35 +3,35 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import * as monaco from 'monaco-editor'
|
||||
import { defineComponent } from "vue";
|
||||
import * as monaco from "monaco-editor";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
}
|
||||
default: "auto",
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value (newValue) {
|
||||
this.editor.setValue(newValue)
|
||||
value(newValue) {
|
||||
this.editor.setValue(newValue);
|
||||
},
|
||||
language(newValue) {
|
||||
this.editor.setModelLanguage(this.editor.getModule(), newValue);
|
||||
},
|
||||
language (newValue) {
|
||||
this.editor.setModelLanguage(this.editor.getModule(), newValue)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
this.editor = monaco.editor.create(this.$refs.editor, {
|
||||
value: this.value,
|
||||
language: this.language
|
||||
})
|
||||
}
|
||||
})
|
||||
language: this.language,
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
<template>
|
||||
<div class="filetree">
|
||||
<file-tree-node
|
||||
:item="tree"
|
||||
@open="path => $emit('open', path)"
|
||||
/>
|
||||
<file-tree-node :item="tree" @open="(path) => $emit('open', path)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, inject, ref } from 'vue'
|
||||
import FileTreeNode from './FileTreeNode.vue'
|
||||
import { defineComponent, inject, ref } from "vue";
|
||||
import FileTreeNode from "./FileTreeNode.vue";
|
||||
|
||||
function unflattenArray (items, toplevelKey = 'root') {
|
||||
const res = { name: toplevelKey, path: '', children: [] }
|
||||
function unflattenArray(items, toplevelKey = "root") {
|
||||
const res = { name: toplevelKey, path: "", children: [] };
|
||||
for (const item of items) {
|
||||
const split = item.split(':')
|
||||
let target = res
|
||||
const split = item.split(":");
|
||||
let target = res;
|
||||
for (const name of split) {
|
||||
let child = target.children.find(c => c.name === name)
|
||||
let child = target.children.find((c) => c.name === name);
|
||||
if (!child) {
|
||||
child = {
|
||||
name,
|
||||
path: target.path + ':' + name,
|
||||
children: []
|
||||
}
|
||||
target.children.push(child)
|
||||
target.children = target.children.sort((c1, c2) => c1.name.localeCompare(c2.name))
|
||||
path: target.path + ":" + name,
|
||||
children: [],
|
||||
};
|
||||
target.children.push(child);
|
||||
target.children = target.children.sort((c1, c2) =>
|
||||
c1.name.localeCompare(c2.name)
|
||||
);
|
||||
}
|
||||
target = child
|
||||
target = child;
|
||||
}
|
||||
target.path = item
|
||||
target.path = item;
|
||||
}
|
||||
return res
|
||||
return res;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: { FileTreeNode },
|
||||
async setup () {
|
||||
const storage = inject('storage')
|
||||
const tree = ref([])
|
||||
async setup() {
|
||||
const storage = inject("storage");
|
||||
const tree = ref([]);
|
||||
await storage.getKeys().then((_keys) => {
|
||||
tree.value = unflattenArray(_keys, 'workspace')
|
||||
})
|
||||
tree.value = unflattenArray(_keys, "workspace");
|
||||
});
|
||||
return {
|
||||
tree
|
||||
}
|
||||
}
|
||||
})
|
||||
tree,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
<template>
|
||||
<li class="filetree-node">
|
||||
<div @click="onOpen">
|
||||
{{ item.name }} <span v-if="isDirectory">[{{ isOpen ? '-' : '+' }}]</span>
|
||||
{{ item.name }} <span v-if="isDirectory">[{{ isOpen ? "-" : "+" }}]</span>
|
||||
</div>
|
||||
<ul v-if="isOpen && item.children.length">
|
||||
<file-tree-node
|
||||
v-for="child of item.children"
|
||||
:key="child.name"
|
||||
:item="child"
|
||||
@open="path => $emit('open', path)"
|
||||
@open="(path) => $emit('open', path)"
|
||||
/>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { defineComponent, ref } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup (props) {
|
||||
setup(props) {
|
||||
return {
|
||||
isOpen: ref((props.item.path || '').split(':').length < 3)
|
||||
}
|
||||
isOpen: ref((props.item.path || "").split(":").length < 3),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDirectory () {
|
||||
return this.item.children.length
|
||||
}
|
||||
isDirectory() {
|
||||
return this.item.children.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onOpen () {
|
||||
onOpen() {
|
||||
if (this.isDirectory) {
|
||||
this.isOpen = !this.isOpen
|
||||
this.isOpen = !this.isOpen;
|
||||
} else {
|
||||
this.$emit('open', this.item.path)
|
||||
this.$emit("open", this.item.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
22
demo/main.js
22
demo/main.js
@@ -1,15 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createStorage } from '../src'
|
||||
import httpDriver from '../src/drivers/http'
|
||||
import App from './App.vue'
|
||||
import { createApp } from "vue";
|
||||
import { createStorage } from "../src";
|
||||
import httpDriver from "../src/drivers/http";
|
||||
import App from "./App.vue";
|
||||
|
||||
function main () {
|
||||
function main() {
|
||||
const storage = createStorage({
|
||||
driver: httpDriver({ base: location.origin + '/storage' })
|
||||
})
|
||||
const app = createApp(App)
|
||||
app.provide('storage', storage)
|
||||
app.mount('#app')
|
||||
driver: httpDriver({ base: location.origin + "/storage" }),
|
||||
});
|
||||
const app = createApp(App);
|
||||
app.provide("storage", storage);
|
||||
app.mount("#app");
|
||||
}
|
||||
|
||||
main()
|
||||
main();
|
||||
|
||||
@@ -9,20 +9,20 @@ import fsdriver from "../src/drivers/fs";
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"node-fetch": "node-fetch/browser"
|
||||
}
|
||||
"node-fetch": "node-fetch/browser",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
{
|
||||
name: "app",
|
||||
configureServer (server) {
|
||||
configureServer(server) {
|
||||
const storage = createStorage();
|
||||
const storageServer = createStorageServer(storage);
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
storage.mount("/src", fsdriver({ base: resolve(__dirname, "..") }));
|
||||
server.middlewares.use("/storage", storageServer.handle);
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
|
||||
type Promisified<T> = Promise<Awaited<T>>
|
||||
type Promisified<T> = Promise<Awaited<T>>;
|
||||
|
||||
export function wrapToPromise<T> (value: T) {
|
||||
export function wrapToPromise<T>(value: T) {
|
||||
if (!value || typeof (value as any).then !== "function") {
|
||||
return Promise.resolve(value) as Promisified<T>;
|
||||
}
|
||||
return value as unknown as Promisified<T>;
|
||||
}
|
||||
|
||||
export function asyncCall<T extends (...arguments_: any) => any>(function_: T, ...arguments_: any[]): Promisified<ReturnType<T>> {
|
||||
export function asyncCall<T extends (...arguments_: any) => any>(
|
||||
function_: T,
|
||||
...arguments_: any[]
|
||||
): Promisified<ReturnType<T>> {
|
||||
try {
|
||||
return wrapToPromise(function_(...arguments_));
|
||||
} catch (error) {
|
||||
@@ -16,11 +19,11 @@ export function asyncCall<T extends (...arguments_: any) => any>(function_: T, .
|
||||
}
|
||||
}
|
||||
|
||||
export function isPrimitive (argument: any) {
|
||||
export function isPrimitive(argument: any) {
|
||||
const type = typeof argument;
|
||||
return argument === null || (type !== "object" && type !== "function");
|
||||
}
|
||||
|
||||
export function stringify (argument: any) {
|
||||
return isPrimitive(argument) ? (argument + "") : JSON.stringify(argument);
|
||||
export function stringify(argument: any) {
|
||||
return isPrimitive(argument) ? argument + "" : JSON.stringify(argument);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createStorage } from "./storage";
|
||||
import { createStorageServer } from "./server";
|
||||
import fsDriver from "./drivers/fs";
|
||||
|
||||
async function main () {
|
||||
async function main() {
|
||||
const arguments_ = mri(process.argv.splice(2));
|
||||
|
||||
if (arguments_.help) {
|
||||
@@ -18,14 +18,14 @@ async function main () {
|
||||
const rootDir = resolve(arguments_._[0] || ".");
|
||||
|
||||
const storage = createStorage({
|
||||
driver: fsDriver({ base: rootDir })
|
||||
driver: fsDriver({ base: rootDir }),
|
||||
});
|
||||
|
||||
const storageServer = createStorageServer(storage);
|
||||
|
||||
await listen(storageServer.handle, {
|
||||
name: "Storage server",
|
||||
port: 8080
|
||||
port: 8080,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +1,62 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
import { defineDriver } from './utils'
|
||||
import { defineDriver } from "./utils";
|
||||
export interface KVOptions {
|
||||
binding?: string | KVNamespace
|
||||
binding?: string | KVNamespace;
|
||||
}
|
||||
|
||||
// https://developers.cloudflare.com/workers/runtime-apis/kv
|
||||
|
||||
export default defineDriver((opts: KVOptions = {}) => {
|
||||
const binding = getBinding(opts.binding)
|
||||
const binding = getBinding(opts.binding);
|
||||
|
||||
async function getKeys(base?: string) {
|
||||
const kvList = await binding.list(base ? { prefix: base } : undefined)
|
||||
return kvList.keys.map(key => key.name)
|
||||
const kvList = await binding.list(base ? { prefix: base } : undefined);
|
||||
return kvList.keys.map((key) => key.name);
|
||||
}
|
||||
|
||||
return {
|
||||
async hasItem(key) {
|
||||
return (await binding.get(key)) !== null
|
||||
return (await binding.get(key)) !== null;
|
||||
},
|
||||
getItem(key) {
|
||||
return binding.get(key)
|
||||
return binding.get(key);
|
||||
},
|
||||
setItem(key, value) {
|
||||
return binding.put(key, value)
|
||||
return binding.put(key, value);
|
||||
},
|
||||
removeItem(key) {
|
||||
return binding.delete(key)
|
||||
return binding.delete(key);
|
||||
},
|
||||
// TODO: use this.getKeys once core is fixed
|
||||
getKeys,
|
||||
async clear() {
|
||||
const keys = await getKeys()
|
||||
await Promise.all(keys.map(key => binding.delete(key)))
|
||||
}
|
||||
}
|
||||
})
|
||||
const keys = await getKeys();
|
||||
await Promise.all(keys.map((key) => binding.delete(key)));
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
function getBinding(binding: KVNamespace | string = "STORAGE") {
|
||||
let bindingName = "[binding]";
|
||||
|
||||
function getBinding(binding: KVNamespace | string = 'STORAGE') {
|
||||
let bindingName = '[binding]'
|
||||
|
||||
if (typeof binding === 'string') {
|
||||
bindingName = binding
|
||||
binding = (globalThis as any)[bindingName] as KVNamespace
|
||||
if (typeof binding === "string") {
|
||||
bindingName = binding;
|
||||
binding = (globalThis as any)[bindingName] as KVNamespace;
|
||||
}
|
||||
|
||||
if (!binding) {
|
||||
throw new Error(`Invalid Cloudflare KV binding '${bindingName}': ${binding}`)
|
||||
throw new Error(
|
||||
`Invalid Cloudflare KV binding '${bindingName}': ${binding}`
|
||||
);
|
||||
}
|
||||
|
||||
for (const key of ['get', 'put', 'delete']) {
|
||||
for (const key of ["get", "put", "delete"]) {
|
||||
if (!(key in binding)) {
|
||||
throw new Error(`Invalid Cloudflare KV binding '${bindingName}': '${key}' key is missing`)
|
||||
throw new Error(
|
||||
`Invalid Cloudflare KV binding '${bindingName}': '${key}' key is missing`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return binding
|
||||
return binding;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { $fetch } from 'ofetch'
|
||||
import { defineDriver } from './utils'
|
||||
import { $fetch } from "ofetch";
|
||||
import { defineDriver } from "./utils";
|
||||
|
||||
const LOG_TAG = '[unstorage] [cloudflare-http] '
|
||||
const LOG_TAG = "[unstorage] [cloudflare-http] ";
|
||||
|
||||
interface KVAuthAPIToken {
|
||||
/**
|
||||
@@ -9,7 +9,7 @@ interface KVAuthAPIToken {
|
||||
* of the Cloudflare console.
|
||||
* @see https://api.cloudflare.com/#getting-started-requests
|
||||
*/
|
||||
apiToken: string
|
||||
apiToken: string;
|
||||
}
|
||||
|
||||
interface KVAuthServiceKey {
|
||||
@@ -19,7 +19,7 @@ interface KVAuthServiceKey {
|
||||
* May be used to authenticate in place of `apiToken` or `apiKey` and `email`.
|
||||
* @see https://api.cloudflare.com/#getting-started-requests
|
||||
*/
|
||||
userServiceKey: string
|
||||
userServiceKey: string;
|
||||
}
|
||||
|
||||
interface KVAuthEmailKey {
|
||||
@@ -27,150 +27,170 @@ interface KVAuthEmailKey {
|
||||
* Email address associated with your account.
|
||||
* Should be used along with `apiKey` to authenticate in place of `apiToken`.
|
||||
*/
|
||||
email: string
|
||||
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
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export type KVHTTPOptions = {
|
||||
/**
|
||||
* Cloudflare account ID (required)
|
||||
*/
|
||||
accountId: string
|
||||
accountId: string;
|
||||
/**
|
||||
* The ID of the KV namespace to target (required)
|
||||
*/
|
||||
namespaceId: string
|
||||
namespaceId: string;
|
||||
/**
|
||||
* The URL of the Cloudflare API.
|
||||
* @default https://api.cloudflare.com
|
||||
*/
|
||||
apiURL?: string
|
||||
} & (KVAuthServiceKey | KVAuthAPIToken | KVAuthEmailKey)
|
||||
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}`
|
||||
}
|
||||
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<KVHTTPOptions>((opts) => {
|
||||
if (!opts.accountId) {
|
||||
throw new Error(LOG_TAG + '`accountId` is required.')
|
||||
throw new Error(LOG_TAG + "`accountId` is required.");
|
||||
}
|
||||
if (!opts.namespaceId) {
|
||||
throw new Error(LOG_TAG + '`namespaceId` is required.')
|
||||
throw new Error(LOG_TAG + "`namespaceId` is required.");
|
||||
}
|
||||
|
||||
let headers: CloudflareAuthorizationHeaders
|
||||
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 }
|
||||
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 }
|
||||
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.'
|
||||
)
|
||||
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 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
|
||||
const res = await kvFetch(`/metadata/${key}`);
|
||||
return res?.success === true;
|
||||
} catch (err) {
|
||||
if (!err.response) { throw err }
|
||||
if (err.response.status === 404) { return false }
|
||||
throw err
|
||||
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())
|
||||
return await kvFetch(`/values/${key}`).then((r) => r.text());
|
||||
} catch (err) {
|
||||
if (!err.response) { throw err }
|
||||
if (err.response.status === 404) { return null }
|
||||
throw err
|
||||
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 })
|
||||
}
|
||||
return await kvFetch(`/values/${key}`, { method: "PUT", body: value });
|
||||
};
|
||||
|
||||
const removeItem = async (key: string) => {
|
||||
return await kvFetch(`/values/${key}`, { method: 'DELETE' })
|
||||
}
|
||||
return await kvFetch(`/values/${key}`, { method: "DELETE" });
|
||||
};
|
||||
|
||||
const getKeys = async (base?: string) => {
|
||||
const keys: string[] = []
|
||||
const keys: string[] = [];
|
||||
|
||||
const params: Record<string, string> = {}
|
||||
const params: Record<string, string> = {};
|
||||
if (base) {
|
||||
params.prefix = base
|
||||
params.prefix = base;
|
||||
}
|
||||
|
||||
const firstPage = await kvFetch('/keys', { params })
|
||||
firstPage.result.forEach(({ name }: { name: string }) => keys.push(name))
|
||||
const firstPage = await kvFetch("/keys", { params });
|
||||
firstPage.result.forEach(({ name }: { name: string }) => keys.push(name));
|
||||
|
||||
const cursor = firstPage.result_info.cursor
|
||||
const cursor = firstPage.result_info.cursor;
|
||||
if (cursor) {
|
||||
params.cursor = cursor
|
||||
params.cursor = cursor;
|
||||
}
|
||||
|
||||
while (params.cursor) {
|
||||
const pageResult = await kvFetch('/keys', { params })
|
||||
pageResult.result.forEach(({ name }: { name: string }) => keys.push(name))
|
||||
const pageCursor = pageResult.result_info.cursor
|
||||
const pageResult = await kvFetch("/keys", { params });
|
||||
pageResult.result.forEach(({ name }: { name: string }) =>
|
||||
keys.push(name)
|
||||
);
|
||||
const pageCursor = pageResult.result_info.cursor;
|
||||
if (pageCursor) {
|
||||
params.cursor = pageCursor
|
||||
params.cursor = pageCursor;
|
||||
} else {
|
||||
params.cursor = undefined
|
||||
|
||||
params.cursor = undefined;
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
|
||||
const clear = async () => {
|
||||
const keys: string[] = await getKeys()
|
||||
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
|
||||
}, [[]])
|
||||
const chunks = keys.reduce(
|
||||
(acc, key, i) => {
|
||||
if (i % 10000 === 0) {
|
||||
acc.push([]);
|
||||
}
|
||||
acc[acc.length - 1].push(key);
|
||||
return acc;
|
||||
},
|
||||
[[]]
|
||||
);
|
||||
// Call bulk delete endpoint with each chunk
|
||||
await Promise.all(chunks.map((chunk) => {
|
||||
return kvFetch('/bulk', {
|
||||
method: 'DELETE',
|
||||
body: { keys: chunk },
|
||||
await Promise.all(
|
||||
chunks.map((chunk) => {
|
||||
return kvFetch("/bulk", {
|
||||
method: "DELETE",
|
||||
body: { keys: chunk },
|
||||
});
|
||||
})
|
||||
}))
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
hasItem,
|
||||
@@ -179,5 +199,5 @@ export default defineDriver<KVHTTPOptions>((opts) => {
|
||||
removeItem,
|
||||
getKeys,
|
||||
clear,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,93 +1,100 @@
|
||||
import { existsSync, promises as fsp } from 'fs'
|
||||
import { resolve, relative, join } from 'path'
|
||||
import { FSWatcher, WatchOptions, watch } from 'chokidar'
|
||||
import { defineDriver } from './utils'
|
||||
import { readFile, writeFile, readdirRecursive, rmRecursive, unlink } from './utils/node-fs'
|
||||
import anymatch from 'anymatch'
|
||||
import { existsSync, promises as fsp } from "fs";
|
||||
import { resolve, relative, join } from "path";
|
||||
import { FSWatcher, WatchOptions, watch } from "chokidar";
|
||||
import { defineDriver } from "./utils";
|
||||
import {
|
||||
readFile,
|
||||
writeFile,
|
||||
readdirRecursive,
|
||||
rmRecursive,
|
||||
unlink,
|
||||
} from "./utils/node-fs";
|
||||
import anymatch from "anymatch";
|
||||
|
||||
export interface FSStorageOptions {
|
||||
base?: string
|
||||
ignore?: string[]
|
||||
watchOptions?: WatchOptions
|
||||
base?: string;
|
||||
ignore?: string[];
|
||||
watchOptions?: WatchOptions;
|
||||
}
|
||||
|
||||
const PATH_TRAVERSE_RE = /\.\.\:|\.\.$/
|
||||
const PATH_TRAVERSE_RE = /\.\.\:|\.\.$/;
|
||||
|
||||
export default defineDriver((opts: FSStorageOptions = {}) => {
|
||||
if (!opts.base) {
|
||||
throw new Error('base is required')
|
||||
throw new Error("base is required");
|
||||
}
|
||||
|
||||
if (!opts.ignore) {
|
||||
opts.ignore = [
|
||||
'**/node_modules/**',
|
||||
'**/.git/**'
|
||||
]
|
||||
opts.ignore = ["**/node_modules/**", "**/.git/**"];
|
||||
}
|
||||
|
||||
opts.base = resolve(opts.base)
|
||||
opts.base = resolve(opts.base);
|
||||
const r = (key: string) => {
|
||||
if (PATH_TRAVERSE_RE.test(key)) {
|
||||
throw new Error('[unstorage] [fs] Invalid key. It should not contain `..` segments: ' + key)
|
||||
throw new Error(
|
||||
"[unstorage] [fs] Invalid key. It should not contain `..` segments: " +
|
||||
key
|
||||
);
|
||||
}
|
||||
const resolved = join(opts.base!, key.replace(/:/g, '/'))
|
||||
return resolved
|
||||
}
|
||||
const resolved = join(opts.base!, key.replace(/:/g, "/"));
|
||||
return resolved;
|
||||
};
|
||||
|
||||
let _watcher: FSWatcher
|
||||
let _watcher: FSWatcher;
|
||||
|
||||
return {
|
||||
hasItem (key) {
|
||||
return existsSync(r(key))
|
||||
hasItem(key) {
|
||||
return existsSync(r(key));
|
||||
},
|
||||
getItem (key) {
|
||||
return readFile(r(key))
|
||||
getItem(key) {
|
||||
return readFile(r(key));
|
||||
},
|
||||
async getMeta (key) {
|
||||
const { atime, mtime, size } = await fsp.stat(r(key))
|
||||
.catch(() => ({ atime: undefined, mtime: undefined, size: undefined }))
|
||||
return { atime, mtime, size }
|
||||
async getMeta(key) {
|
||||
const { atime, mtime, size } = await fsp
|
||||
.stat(r(key))
|
||||
.catch(() => ({ atime: undefined, mtime: undefined, size: undefined }));
|
||||
return { atime, mtime, size };
|
||||
},
|
||||
setItem (key, value) {
|
||||
return writeFile(r(key), value)
|
||||
setItem(key, value) {
|
||||
return writeFile(r(key), value);
|
||||
},
|
||||
removeItem (key) {
|
||||
return unlink(r(key))
|
||||
removeItem(key) {
|
||||
return unlink(r(key));
|
||||
},
|
||||
getKeys () {
|
||||
return readdirRecursive(r('.'), anymatch(opts.ignore || []))
|
||||
getKeys() {
|
||||
return readdirRecursive(r("."), anymatch(opts.ignore || []));
|
||||
},
|
||||
async clear () {
|
||||
await rmRecursive(r('.'))
|
||||
async clear() {
|
||||
await rmRecursive(r("."));
|
||||
},
|
||||
async dispose() {
|
||||
if (_watcher) {
|
||||
await _watcher.close()
|
||||
await _watcher.close();
|
||||
}
|
||||
},
|
||||
},
|
||||
watch(callback) {
|
||||
if (_watcher) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
_watcher = watch(opts.base!, {
|
||||
ignoreInitial: true,
|
||||
ignored: opts.ignore,
|
||||
...opts.watchOptions
|
||||
...opts.watchOptions,
|
||||
})
|
||||
.on('ready', () => {
|
||||
resolve(() => _watcher.close().then(() => _watcher = undefined))
|
||||
.on("ready", () => {
|
||||
resolve(() => _watcher.close().then(() => (_watcher = undefined)));
|
||||
})
|
||||
.on('error', reject)
|
||||
.on('all', (eventName, path) => {
|
||||
path = relative(opts.base!, path)
|
||||
if (eventName === 'change' || eventName === 'add') {
|
||||
callback('update', path)
|
||||
} else if (eventName === 'unlink') {
|
||||
callback('remove', path)
|
||||
.on("error", reject)
|
||||
.on("all", (eventName, path) => {
|
||||
path = relative(opts.base!, path);
|
||||
if (eventName === "change" || eventName === "add") {
|
||||
callback("update", path);
|
||||
} else if (eventName === "unlink") {
|
||||
callback("remove", path);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,141 +1,148 @@
|
||||
import { defineDriver } from './utils'
|
||||
import { $fetch } from 'ofetch'
|
||||
import { withTrailingSlash, joinURL } from 'ufo'
|
||||
import { defineDriver } from "./utils";
|
||||
import { $fetch } from "ofetch";
|
||||
import { withTrailingSlash, joinURL } from "ufo";
|
||||
|
||||
export interface GithubOptions {
|
||||
/**
|
||||
* The name of the repository. (e.g. `username/my-repo`)
|
||||
* Required
|
||||
*/
|
||||
repo: string
|
||||
repo: string;
|
||||
/**
|
||||
* The branch to fetch. (e.g. `dev`)
|
||||
* @default "main"
|
||||
*/
|
||||
branch: string
|
||||
branch: string;
|
||||
/**
|
||||
* @default ""
|
||||
*/
|
||||
dir: string
|
||||
dir: string;
|
||||
/**
|
||||
* @default 600
|
||||
*/
|
||||
ttl: number
|
||||
ttl: number;
|
||||
/**
|
||||
* Github API token (recommended)
|
||||
*/
|
||||
token?: string
|
||||
token?: string;
|
||||
/**
|
||||
* @default "https://api.github.com"
|
||||
*/
|
||||
apiURL?: string
|
||||
apiURL?: string;
|
||||
/**
|
||||
* @default "https://raw.githubusercontent.com"
|
||||
*/
|
||||
cdnURL?: string
|
||||
cdnURL?: string;
|
||||
}
|
||||
|
||||
const defaultOptions: GithubOptions = {
|
||||
repo: null,
|
||||
branch: 'main',
|
||||
branch: "main",
|
||||
ttl: 600,
|
||||
dir: '',
|
||||
apiURL: 'https://api.github.com',
|
||||
cdnURL: 'https://raw.githubusercontent.com'
|
||||
}
|
||||
dir: "",
|
||||
apiURL: "https://api.github.com",
|
||||
cdnURL: "https://raw.githubusercontent.com",
|
||||
};
|
||||
|
||||
export default defineDriver((_opts: GithubOptions) => {
|
||||
const opts = { ...defaultOptions, ..._opts }
|
||||
const rawUrl = joinURL(opts.cdnURL, opts.repo, opts.branch, opts.dir)
|
||||
const opts = { ...defaultOptions, ..._opts };
|
||||
const rawUrl = joinURL(opts.cdnURL, opts.repo, opts.branch, opts.dir);
|
||||
|
||||
let files = {}
|
||||
let lastCheck = 0
|
||||
let syncPromise: Promise<any>
|
||||
let files = {};
|
||||
let lastCheck = 0;
|
||||
let syncPromise: Promise<any>;
|
||||
|
||||
if (!opts.repo) {
|
||||
throw new Error('[unstorage] [github] Missing required option "repo"')
|
||||
throw new Error('[unstorage] [github] Missing required option "repo"');
|
||||
}
|
||||
|
||||
const syncFiles = async () => {
|
||||
if ((lastCheck + opts.ttl * 1000) > Date.now()) {
|
||||
return
|
||||
if (lastCheck + opts.ttl * 1000 > Date.now()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!syncPromise) {
|
||||
syncPromise = fetchFiles(opts)
|
||||
syncPromise = fetchFiles(opts);
|
||||
}
|
||||
|
||||
files = await syncPromise
|
||||
lastCheck = Date.now()
|
||||
syncPromise = undefined
|
||||
}
|
||||
files = await syncPromise;
|
||||
lastCheck = Date.now();
|
||||
syncPromise = undefined;
|
||||
};
|
||||
|
||||
return {
|
||||
async getKeys() {
|
||||
await syncFiles()
|
||||
return Object.keys(files)
|
||||
await syncFiles();
|
||||
return Object.keys(files);
|
||||
},
|
||||
async hasItem(key) {
|
||||
await syncFiles()
|
||||
return key in files
|
||||
await syncFiles();
|
||||
return key in files;
|
||||
},
|
||||
async getItem(key) {
|
||||
await syncFiles()
|
||||
await syncFiles();
|
||||
|
||||
const item = files[key]
|
||||
const item = files[key];
|
||||
|
||||
if (!item) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!item.body) {
|
||||
try {
|
||||
item.body = await $fetch(key.replace(/:/g, '/'), {
|
||||
item.body = await $fetch(key.replace(/:/g, "/"), {
|
||||
baseURL: rawUrl,
|
||||
headers: {
|
||||
Authorization: opts.token ? `token ${opts.token}` : undefined
|
||||
}
|
||||
})
|
||||
Authorization: opts.token ? `token ${opts.token}` : undefined,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`[unstorage] [github] Failed to fetch "${key}"`, { cause: err })
|
||||
throw new Error(`[unstorage] [github] Failed to fetch "${key}"`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
return item.body
|
||||
return item.body;
|
||||
},
|
||||
async getMeta(key) {
|
||||
await syncFiles()
|
||||
const item = files[key]
|
||||
return item ? item.meta : null
|
||||
}
|
||||
}
|
||||
})
|
||||
await syncFiles();
|
||||
const item = files[key];
|
||||
return item ? item.meta : null;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
async function fetchFiles(opts: GithubOptions) {
|
||||
const prefix = withTrailingSlash(opts.dir).replace(/^\//, '')
|
||||
const files = {}
|
||||
const prefix = withTrailingSlash(opts.dir).replace(/^\//, "");
|
||||
const files = {};
|
||||
try {
|
||||
const trees = await $fetch(`/repos/${opts.repo}/git/trees/${opts.branch}?recursive=1`, {
|
||||
baseURL: opts.apiURL,
|
||||
headers: {
|
||||
Authorization: opts.token ? `token ${opts.token}` : undefined
|
||||
const trees = await $fetch(
|
||||
`/repos/${opts.repo}/git/trees/${opts.branch}?recursive=1`,
|
||||
{
|
||||
baseURL: opts.apiURL,
|
||||
headers: {
|
||||
Authorization: opts.token ? `token ${opts.token}` : undefined,
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
for (const node of trees.tree) {
|
||||
if (node.type !== 'blob' || !node.path.startsWith(prefix)) {
|
||||
continue
|
||||
if (node.type !== "blob" || !node.path.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
const key = node.path.substring(prefix.length).replace(/\//g, ':')
|
||||
const key = node.path.substring(prefix.length).replace(/\//g, ":");
|
||||
files[key] = {
|
||||
meta: {
|
||||
sha: node.sha,
|
||||
mode: node.mode,
|
||||
size: node.size
|
||||
}
|
||||
}
|
||||
size: node.size,
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`[unstorage] [github] Failed to fetch git tree`, { cause: err })
|
||||
throw new Error(`[unstorage] [github] Failed to fetch git tree`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
return files
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -1,47 +1,49 @@
|
||||
import { defineDriver } from './utils'
|
||||
import { stringify } from './utils'
|
||||
import { $fetch } from 'ofetch'
|
||||
import { joinURL } from 'ufo'
|
||||
import { defineDriver } from "./utils";
|
||||
import { stringify } from "./utils";
|
||||
import { $fetch } from "ofetch";
|
||||
import { joinURL } from "ufo";
|
||||
|
||||
export interface HTTPOptions {
|
||||
base?: string
|
||||
base?: string;
|
||||
}
|
||||
|
||||
export default defineDriver((opts: HTTPOptions = {}) => {
|
||||
const r = (key: string) => joinURL(opts.base!, key.replace(/:/g, '/'))
|
||||
const r = (key: string) => joinURL(opts.base!, key.replace(/:/g, "/"));
|
||||
|
||||
return {
|
||||
hasItem(key) {
|
||||
return $fetch(r(key), { method: 'HEAD' })
|
||||
return $fetch(r(key), { method: "HEAD" })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
.catch(() => false);
|
||||
},
|
||||
async getItem (key) {
|
||||
const value = await $fetch(r(key))
|
||||
return value
|
||||
async getItem(key) {
|
||||
const value = await $fetch(r(key));
|
||||
return value;
|
||||
},
|
||||
async getMeta (key) {
|
||||
const res = await $fetch.raw(r(key), { method: 'HEAD' })
|
||||
let mtime = undefined
|
||||
const _lastModified = res.headers.get('last-modified')
|
||||
if (_lastModified) { mtime = new Date(_lastModified) }
|
||||
async getMeta(key) {
|
||||
const res = await $fetch.raw(r(key), { method: "HEAD" });
|
||||
let mtime = undefined;
|
||||
const _lastModified = res.headers.get("last-modified");
|
||||
if (_lastModified) {
|
||||
mtime = new Date(_lastModified);
|
||||
}
|
||||
return {
|
||||
status: res.status,
|
||||
mtime
|
||||
}
|
||||
mtime,
|
||||
};
|
||||
},
|
||||
async setItem(key, value) {
|
||||
await $fetch(r(key), { method: 'PUT', body: stringify(value) })
|
||||
await $fetch(r(key), { method: "PUT", body: stringify(value) });
|
||||
},
|
||||
async removeItem (key) {
|
||||
await $fetch(r(key), { method: 'DELETE' })
|
||||
async removeItem(key) {
|
||||
await $fetch(r(key), { method: "DELETE" });
|
||||
},
|
||||
async getKeys() {
|
||||
const value = await $fetch(r(''))
|
||||
return Array.isArray(value) ? value : []
|
||||
const value = await $fetch(r(""));
|
||||
return Array.isArray(value) ? value : [];
|
||||
},
|
||||
clear() {
|
||||
// Not supported
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,69 +1,68 @@
|
||||
import { defineDriver } from './utils'
|
||||
import { defineDriver } from "./utils";
|
||||
|
||||
export interface LocalStorageOptions {
|
||||
base?: string
|
||||
window?: typeof window
|
||||
localStorage?: typeof window.localStorage
|
||||
base?: string;
|
||||
window?: typeof window;
|
||||
localStorage?: typeof window.localStorage;
|
||||
}
|
||||
|
||||
export default defineDriver((opts: LocalStorageOptions = {}) => {
|
||||
if (!opts.window) {
|
||||
opts.window = typeof window !== 'undefined' ? window : undefined
|
||||
opts.window = typeof window !== "undefined" ? window : undefined;
|
||||
}
|
||||
if (!opts.localStorage) {
|
||||
opts.localStorage = opts.window?.localStorage
|
||||
opts.localStorage = opts.window?.localStorage;
|
||||
}
|
||||
if (!opts.localStorage) {
|
||||
throw new Error('localStorage not available')
|
||||
throw new Error("localStorage not available");
|
||||
}
|
||||
|
||||
const r = (key: string) => (opts.base ? opts.base + ':' : '') + key
|
||||
const r = (key: string) => (opts.base ? opts.base + ":" : "") + key;
|
||||
|
||||
|
||||
let _storageListener: (ev: StorageEvent) => void
|
||||
let _storageListener: (ev: StorageEvent) => void;
|
||||
|
||||
return {
|
||||
hasItem (key) {
|
||||
return Object.prototype.hasOwnProperty.call(opts.localStorage!, r(key))
|
||||
hasItem(key) {
|
||||
return Object.prototype.hasOwnProperty.call(opts.localStorage!, r(key));
|
||||
},
|
||||
getItem (key) {
|
||||
return opts.localStorage!.getItem(r(key))
|
||||
getItem(key) {
|
||||
return opts.localStorage!.getItem(r(key));
|
||||
},
|
||||
setItem (key, value) {
|
||||
return opts.localStorage!.setItem(r(key), value)
|
||||
setItem(key, value) {
|
||||
return opts.localStorage!.setItem(r(key), value);
|
||||
},
|
||||
removeItem (key) {
|
||||
return opts.localStorage!.removeItem(r(key))
|
||||
removeItem(key) {
|
||||
return opts.localStorage!.removeItem(r(key));
|
||||
},
|
||||
getKeys () {
|
||||
return Object.keys(opts.localStorage!)
|
||||
getKeys() {
|
||||
return Object.keys(opts.localStorage!);
|
||||
},
|
||||
clear() {
|
||||
if (!opts.base) {
|
||||
opts.localStorage!.clear()
|
||||
opts.localStorage!.clear();
|
||||
} else {
|
||||
for (const key of Object.keys(opts.localStorage!)) {
|
||||
opts.localStorage?.removeItem(key)
|
||||
opts.localStorage?.removeItem(key);
|
||||
}
|
||||
}
|
||||
if (opts.window && _storageListener) {
|
||||
opts.window.removeEventListener('storage', _storageListener)
|
||||
opts.window.removeEventListener("storage", _storageListener);
|
||||
}
|
||||
},
|
||||
watch(callback) {
|
||||
if (!opts.window) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
_storageListener = (ev: StorageEvent) => {
|
||||
if (ev.key) {
|
||||
callback(ev.newValue ? 'update' : 'remove', ev.key)
|
||||
callback(ev.newValue ? "update" : "remove", ev.key);
|
||||
}
|
||||
}
|
||||
opts.window.addEventListener('storage', _storageListener)
|
||||
};
|
||||
opts.window.addEventListener("storage", _storageListener);
|
||||
return () => {
|
||||
opts.window.removeEventListener('storage', _storageListener)
|
||||
_storageListener = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
opts.window.removeEventListener("storage", _storageListener);
|
||||
_storageListener = undefined;
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { defineDriver } from './utils'
|
||||
import type { StorageValue } from '../types'
|
||||
import { defineDriver } from "./utils";
|
||||
import type { StorageValue } from "../types";
|
||||
|
||||
export default defineDriver(() => {
|
||||
const data = new Map<string, StorageValue>()
|
||||
const data = new Map<string, StorageValue>();
|
||||
|
||||
return {
|
||||
hasItem (key) {
|
||||
return data.has(key)
|
||||
hasItem(key) {
|
||||
return data.has(key);
|
||||
},
|
||||
getItem (key) {
|
||||
return data.get(key) || null
|
||||
getItem(key) {
|
||||
return data.get(key) || null;
|
||||
},
|
||||
setItem(key, value) {
|
||||
data.set(key, value)
|
||||
data.set(key, value);
|
||||
},
|
||||
removeItem (key) {
|
||||
data.delete(key)
|
||||
removeItem(key) {
|
||||
data.delete(key);
|
||||
},
|
||||
getKeys() {
|
||||
return Array.from(data.keys())
|
||||
return Array.from(data.keys());
|
||||
},
|
||||
clear() {
|
||||
data.clear()
|
||||
data.clear();
|
||||
},
|
||||
dispose() {
|
||||
data.clear()
|
||||
}
|
||||
}
|
||||
})
|
||||
data.clear();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,69 +1,75 @@
|
||||
import { defineDriver } from './utils'
|
||||
import type { Driver } from '../types'
|
||||
import { normalizeKey } from './utils'
|
||||
import { defineDriver } from "./utils";
|
||||
import type { Driver } from "../types";
|
||||
import { normalizeKey } from "./utils";
|
||||
|
||||
export interface OverlayStorageOptions {
|
||||
layers: Driver[]
|
||||
layers: Driver[];
|
||||
}
|
||||
|
||||
const OVERLAY_REMOVED = '__OVERLAY_REMOVED__'
|
||||
const OVERLAY_REMOVED = "__OVERLAY_REMOVED__";
|
||||
|
||||
export default defineDriver((options: OverlayStorageOptions) => {
|
||||
return {
|
||||
async hasItem (key) {
|
||||
async hasItem(key) {
|
||||
for (const layer of options.layers) {
|
||||
if (await layer.hasItem(key)) {
|
||||
if (layer === options.layers[0]) {
|
||||
if (await options.layers[0]?.getItem(key) === OVERLAY_REMOVED) {
|
||||
return false
|
||||
if ((await options.layers[0]?.getItem(key)) === OVERLAY_REMOVED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
},
|
||||
async getItem (key) {
|
||||
async getItem(key) {
|
||||
for (const layer of options.layers) {
|
||||
const value = await layer.getItem(key)
|
||||
const value = await layer.getItem(key);
|
||||
if (value === OVERLAY_REMOVED) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
if (value !== null) {
|
||||
return value
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
},
|
||||
// TODO: Support native meta
|
||||
// async getMeta (key) {},
|
||||
async setItem(key, value) {
|
||||
await options.layers[0]?.setItem(key, value)
|
||||
await options.layers[0]?.setItem(key, value);
|
||||
},
|
||||
async removeItem (key) {
|
||||
await options.layers[0]?.setItem(key, OVERLAY_REMOVED)
|
||||
async removeItem(key) {
|
||||
await options.layers[0]?.setItem(key, OVERLAY_REMOVED);
|
||||
},
|
||||
async getKeys(base) {
|
||||
const allKeys = await Promise.all(options.layers.map(async layer => {
|
||||
const keys = await layer.getKeys(base)
|
||||
return keys.map(key => normalizeKey(key))
|
||||
}))
|
||||
const uniqueKeys = Array.from(new Set(allKeys.flat()))
|
||||
const existingKeys = await Promise.all(uniqueKeys.map(async key => {
|
||||
if (await options.layers[0]?.getItem(key) === OVERLAY_REMOVED) {
|
||||
return false
|
||||
}
|
||||
return key
|
||||
}))
|
||||
return existingKeys.filter(Boolean) as string[]
|
||||
const allKeys = await Promise.all(
|
||||
options.layers.map(async (layer) => {
|
||||
const keys = await layer.getKeys(base);
|
||||
return keys.map((key) => normalizeKey(key));
|
||||
})
|
||||
);
|
||||
const uniqueKeys = Array.from(new Set(allKeys.flat()));
|
||||
const existingKeys = await Promise.all(
|
||||
uniqueKeys.map(async (key) => {
|
||||
if ((await options.layers[0]?.getItem(key)) === OVERLAY_REMOVED) {
|
||||
return false;
|
||||
}
|
||||
return key;
|
||||
})
|
||||
);
|
||||
return existingKeys.filter(Boolean) as string[];
|
||||
},
|
||||
async dispose() {
|
||||
// TODO: Graceful error handling
|
||||
await Promise.all(options.layers.map(async layer => {
|
||||
if (layer.dispose) {
|
||||
await layer.dispose()
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
await Promise.all(
|
||||
options.layers.map(async (layer) => {
|
||||
if (layer.dispose) {
|
||||
await layer.dispose();
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,41 +1,43 @@
|
||||
import { defineDriver } from './utils'
|
||||
import Redis, { RedisOptions as _RedisOptions } from 'ioredis'
|
||||
import { defineDriver } from "./utils";
|
||||
import Redis, { RedisOptions as _RedisOptions } from "ioredis";
|
||||
|
||||
export interface RedisOptions extends _RedisOptions {
|
||||
base: string
|
||||
url: string
|
||||
base: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export default defineDriver<RedisOptions>((_opts) => {
|
||||
const opts = { lazyConnect: true, ..._opts }
|
||||
const redis = opts.url ? new Redis(opts?.url, opts) : new Redis(opts)
|
||||
const opts = { lazyConnect: true, ..._opts };
|
||||
const redis = opts.url ? new Redis(opts?.url, opts) : new Redis(opts);
|
||||
|
||||
let base = opts?.base || ''
|
||||
if (base && !base.endsWith(':')) { base += ':' }
|
||||
const r = (key: string) => base + key
|
||||
let base = opts?.base || "";
|
||||
if (base && !base.endsWith(":")) {
|
||||
base += ":";
|
||||
}
|
||||
const r = (key: string) => base + key;
|
||||
|
||||
return {
|
||||
hasItem (key) {
|
||||
return redis.exists(r(key)).then(Boolean)
|
||||
hasItem(key) {
|
||||
return redis.exists(r(key)).then(Boolean);
|
||||
},
|
||||
getItem (key) {
|
||||
return redis.get(r(key))
|
||||
getItem(key) {
|
||||
return redis.get(r(key));
|
||||
},
|
||||
setItem(key, value) {
|
||||
return redis.set(r(key), value).then(() => {})
|
||||
return redis.set(r(key), value).then(() => {});
|
||||
},
|
||||
removeItem (key) {
|
||||
return redis.del(r(key)).then(() => {})
|
||||
removeItem(key) {
|
||||
return redis.del(r(key)).then(() => {});
|
||||
},
|
||||
getKeys() {
|
||||
return redis.keys(r('*'))
|
||||
return redis.keys(r("*"));
|
||||
},
|
||||
async clear() {
|
||||
const keys = await redis.keys(r('*'))
|
||||
return redis.del(keys.map(key => r(key))).then(() => {})
|
||||
const keys = await redis.keys(r("*"));
|
||||
return redis.del(keys.map((key) => r(key))).then(() => {});
|
||||
},
|
||||
dispose() {
|
||||
return redis.disconnect()
|
||||
}
|
||||
}
|
||||
})
|
||||
return redis.disconnect();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import type { Driver } from '../../types'
|
||||
import type { Driver } from "../../types";
|
||||
|
||||
type DriverFactory<T> = (opts?: T) => Driver
|
||||
type DriverFactory<T> = (opts?: T) => Driver;
|
||||
|
||||
export function defineDriver<T = any>(factory: DriverFactory<T>): DriverFactory<T> {
|
||||
return factory
|
||||
export function defineDriver<T = any>(
|
||||
factory: DriverFactory<T>
|
||||
): DriverFactory<T> {
|
||||
return factory;
|
||||
}
|
||||
|
||||
export function isPrimitive (arg: any) {
|
||||
const type = typeof arg
|
||||
return arg === null || (type !== 'object' && type !== 'function')
|
||||
export function isPrimitive(arg: any) {
|
||||
const type = typeof arg;
|
||||
return arg === null || (type !== "object" && type !== "function");
|
||||
}
|
||||
|
||||
export function stringify (arg: any) {
|
||||
return isPrimitive(arg) ? (arg + '') : JSON.stringify(arg)
|
||||
export function stringify(arg: any) {
|
||||
return isPrimitive(arg) ? arg + "" : JSON.stringify(arg);
|
||||
}
|
||||
|
||||
export function normalizeKey (key: string | undefined): string {
|
||||
if (!key) { return '' }
|
||||
return key.replace(/[/\\]/g, ':').replace(/^:|:$/g, '')
|
||||
export function normalizeKey(key: string | undefined): string {
|
||||
if (!key) {
|
||||
return "";
|
||||
}
|
||||
return key.replace(/[/\\]/g, ":").replace(/^:|:$/g, "");
|
||||
}
|
||||
|
||||
@@ -1,69 +1,81 @@
|
||||
import { Dirent, existsSync, promises as fsPromises } from 'fs'
|
||||
import { resolve, dirname } from 'path'
|
||||
import { Dirent, existsSync, promises as fsPromises } from "fs";
|
||||
import { resolve, dirname } from "path";
|
||||
|
||||
function ignoreNotfound (err: any) {
|
||||
return (err.code === 'ENOENT' || err.code === 'EISDIR') ? null : err
|
||||
function ignoreNotfound(err: any) {
|
||||
return err.code === "ENOENT" || err.code === "EISDIR" ? null : err;
|
||||
}
|
||||
|
||||
function ignoreExists (err: any) {
|
||||
return (err.code === 'EEXIST') ? null : err
|
||||
function ignoreExists(err: any) {
|
||||
return err.code === "EEXIST" ? null : err;
|
||||
}
|
||||
|
||||
export async function writeFile (path: string, data: string) {
|
||||
await ensuredir(dirname(path))
|
||||
return fsPromises.writeFile(path, data, 'utf8')
|
||||
export async function writeFile(path: string, data: string) {
|
||||
await ensuredir(dirname(path));
|
||||
return fsPromises.writeFile(path, data, "utf8");
|
||||
}
|
||||
|
||||
export function readFile (path: string) {
|
||||
return fsPromises.readFile(path, 'utf8').catch(ignoreNotfound)
|
||||
export function readFile(path: string) {
|
||||
return fsPromises.readFile(path, "utf8").catch(ignoreNotfound);
|
||||
}
|
||||
|
||||
export function stat (path: string) {
|
||||
return fsPromises.stat(path).catch(ignoreNotfound)
|
||||
export function stat(path: string) {
|
||||
return fsPromises.stat(path).catch(ignoreNotfound);
|
||||
}
|
||||
|
||||
export function unlink (path: string) {
|
||||
return fsPromises.unlink(path).catch(ignoreNotfound)
|
||||
export function unlink(path: string) {
|
||||
return fsPromises.unlink(path).catch(ignoreNotfound);
|
||||
}
|
||||
|
||||
export function readdir (dir: string): Promise<Dirent[]> {
|
||||
return fsPromises.readdir(dir, { withFileTypes: true }).catch(ignoreNotfound).then(r => r || [])
|
||||
export function readdir(dir: string): Promise<Dirent[]> {
|
||||
return fsPromises
|
||||
.readdir(dir, { withFileTypes: true })
|
||||
.catch(ignoreNotfound)
|
||||
.then((r) => r || []);
|
||||
}
|
||||
|
||||
export async function ensuredir (dir: string) {
|
||||
if (existsSync(dir)) { return }
|
||||
await ensuredir(dirname(dir)).catch(ignoreExists)
|
||||
await fsPromises.mkdir(dir).catch(ignoreExists)
|
||||
}
|
||||
|
||||
export async function readdirRecursive(dir: string, ignore?: (p: string) => boolean) {
|
||||
if (ignore && ignore(dir)) {
|
||||
return []
|
||||
export async function ensuredir(dir: string) {
|
||||
if (existsSync(dir)) {
|
||||
return;
|
||||
}
|
||||
const entries: Dirent[] = await readdir(dir)
|
||||
const files: string[] = []
|
||||
await Promise.all(entries.map(async (entry) => {
|
||||
const entryPath = resolve(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
const dirFiles = await readdirRecursive(entryPath, ignore)
|
||||
files.push(...dirFiles.map(f => entry.name + '/' + f))
|
||||
} else {
|
||||
if (ignore && !ignore(entry.name)) {
|
||||
files.push(entry.name)
|
||||
}
|
||||
}
|
||||
}))
|
||||
return files
|
||||
await ensuredir(dirname(dir)).catch(ignoreExists);
|
||||
await fsPromises.mkdir(dir).catch(ignoreExists);
|
||||
}
|
||||
|
||||
export async function rmRecursive (dir: string) {
|
||||
const entries = await readdir(dir)
|
||||
await Promise.all(entries.map((entry) => {
|
||||
const entryPath = resolve(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
return rmRecursive(entryPath).then(() => fsPromises.rmdir(entryPath))
|
||||
} else {
|
||||
return fsPromises.unlink(entryPath)
|
||||
}
|
||||
}))
|
||||
export async function readdirRecursive(
|
||||
dir: string,
|
||||
ignore?: (p: string) => boolean
|
||||
) {
|
||||
if (ignore && ignore(dir)) {
|
||||
return [];
|
||||
}
|
||||
const entries: Dirent[] = await readdir(dir);
|
||||
const files: string[] = [];
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const entryPath = resolve(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const dirFiles = await readdirRecursive(entryPath, ignore);
|
||||
files.push(...dirFiles.map((f) => entry.name + "/" + f));
|
||||
} else {
|
||||
if (ignore && !ignore(entry.name)) {
|
||||
files.push(entry.name);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
return files;
|
||||
}
|
||||
|
||||
export async function rmRecursive(dir: string) {
|
||||
const entries = await readdir(dir);
|
||||
await Promise.all(
|
||||
entries.map((entry) => {
|
||||
const entryPath = resolve(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return rmRecursive(entryPath).then(() => fsPromises.rmdir(entryPath));
|
||||
} else {
|
||||
return fsPromises.unlink(entryPath);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const builtinDrivers = {
|
||||
localstorage: "unstorage/drivers/localstorage",
|
||||
memory: "unstorage/drivers/memory",
|
||||
overlay: "unstorage/drivers/overlay",
|
||||
redis: "unstorage/drivers/redis"
|
||||
redis: "unstorage/drivers/redis",
|
||||
};
|
||||
|
||||
export type BuiltinDriverName = keyof typeof builtinDrivers
|
||||
export type BuiltinDriverName = keyof typeof builtinDrivers;
|
||||
|
||||
@@ -1,56 +1,71 @@
|
||||
import type { RequestListener } from "node:http";
|
||||
import { createApp, createError, readBody, eventHandler, toNodeListener } from "h3";
|
||||
import {
|
||||
createApp,
|
||||
createError,
|
||||
readBody,
|
||||
eventHandler,
|
||||
toNodeListener,
|
||||
} from "h3";
|
||||
import { Storage } from "./types";
|
||||
import { stringify } from "./_utils";
|
||||
|
||||
export interface StorageServerOptions {}
|
||||
|
||||
export interface StorageServer {
|
||||
handle: RequestListener
|
||||
handle: RequestListener;
|
||||
}
|
||||
|
||||
export function createStorageServer (storage: Storage, _options: StorageServerOptions = {}): StorageServer {
|
||||
export function createStorageServer(
|
||||
storage: Storage,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_options: StorageServerOptions = {}
|
||||
): StorageServer {
|
||||
const app = createApp();
|
||||
app.use(eventHandler(async (event) => {
|
||||
// GET => getItem
|
||||
if (event.req.method === "GET") {
|
||||
const value = await storage.getItem(event.req.url!);
|
||||
if (!value) {
|
||||
const keys = await storage.getKeys(event.req.url);
|
||||
return keys.map(key => key.replace(/:/g, "/"));
|
||||
}
|
||||
return stringify(value);
|
||||
}
|
||||
// HEAD => hasItem + meta (mtime)
|
||||
if (event.req.method === "HEAD") {
|
||||
const _hasItem = await storage.hasItem(event.req.url!);
|
||||
event.res.statusCode = _hasItem ? 200 : 404;
|
||||
if (_hasItem) {
|
||||
const meta = await storage.getMeta(event.req.url!);
|
||||
if (meta.mtime) {
|
||||
event.res.setHeader("Last-Modified", new Date(meta.mtime).toUTCString());
|
||||
app.use(
|
||||
eventHandler(async (event) => {
|
||||
// GET => getItem
|
||||
if (event.req.method === "GET") {
|
||||
const value = await storage.getItem(event.req.url!);
|
||||
if (!value) {
|
||||
const keys = await storage.getKeys(event.req.url);
|
||||
return keys.map((key) => key.replace(/:/g, "/"));
|
||||
}
|
||||
return stringify(value);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
// PUT => setItem
|
||||
if (event.req.method === "PUT") {
|
||||
const value = await readBody(event);
|
||||
await storage.setItem(event.req.url!, value);
|
||||
return "OK";
|
||||
}
|
||||
// DELETE => removeItem
|
||||
if (event.req.method === "DELETE") {
|
||||
await storage.removeItem(event.req.url!);
|
||||
return "OK";
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 405,
|
||||
statusMessage: "Method Not Allowed"
|
||||
});
|
||||
}));
|
||||
// HEAD => hasItem + meta (mtime)
|
||||
if (event.req.method === "HEAD") {
|
||||
const _hasItem = await storage.hasItem(event.req.url!);
|
||||
event.res.statusCode = _hasItem ? 200 : 404;
|
||||
if (_hasItem) {
|
||||
const meta = await storage.getMeta(event.req.url!);
|
||||
if (meta.mtime) {
|
||||
event.res.setHeader(
|
||||
"Last-Modified",
|
||||
new Date(meta.mtime).toUTCString()
|
||||
);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
// PUT => setItem
|
||||
if (event.req.method === "PUT") {
|
||||
const value = await readBody(event);
|
||||
await storage.setItem(event.req.url!, value);
|
||||
return "OK";
|
||||
}
|
||||
// DELETE => removeItem
|
||||
if (event.req.method === "DELETE") {
|
||||
await storage.removeItem(event.req.url!);
|
||||
return "OK";
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 405,
|
||||
statusMessage: "Method Not Allowed",
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
handle: toNodeListener(app)
|
||||
handle: toNodeListener(app),
|
||||
};
|
||||
}
|
||||
|
||||
190
src/storage.ts
190
src/storage.ts
@@ -1,28 +1,35 @@
|
||||
import destr from "destr";
|
||||
import type { Storage, Driver, WatchCallback, Unwatch, StorageValue } from "./types";
|
||||
import type {
|
||||
Storage,
|
||||
Driver,
|
||||
WatchCallback,
|
||||
Unwatch,
|
||||
StorageValue,
|
||||
WatchEvent,
|
||||
} from "./types";
|
||||
import memory from "./drivers/memory";
|
||||
import { asyncCall, stringify } from "./_utils";
|
||||
import { normalizeKey, normalizeBaseKey } from "./utils";
|
||||
|
||||
interface StorageCTX {
|
||||
mounts: Record<string, Driver>
|
||||
mountpoints: string[]
|
||||
watching: boolean
|
||||
unwatch: Record<string, Unwatch>
|
||||
watchListeners: Function[]
|
||||
mounts: Record<string, Driver>;
|
||||
mountpoints: string[];
|
||||
watching: boolean;
|
||||
unwatch: Record<string, Unwatch>;
|
||||
watchListeners: ((event: WatchEvent, key: string) => void)[];
|
||||
}
|
||||
|
||||
export interface CreateStorageOptions {
|
||||
driver?: Driver
|
||||
driver?: Driver;
|
||||
}
|
||||
|
||||
export function createStorage (options: CreateStorageOptions = {}): Storage {
|
||||
export function createStorage(options: CreateStorageOptions = {}): Storage {
|
||||
const context: StorageCTX = {
|
||||
mounts: { "": options.driver || memory() },
|
||||
mountpoints: [""],
|
||||
watching: false,
|
||||
watchListeners: [],
|
||||
unwatch: {}
|
||||
unwatch: {},
|
||||
};
|
||||
|
||||
const getMount = (key: string) => {
|
||||
@@ -30,28 +37,37 @@ export function createStorage (options: CreateStorageOptions = {}): Storage {
|
||||
if (key.startsWith(base)) {
|
||||
return {
|
||||
relativeKey: key.slice(base.length),
|
||||
driver: context.mounts[base]
|
||||
driver: context.mounts[base],
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
relativeKey: key,
|
||||
driver: context.mounts[""]
|
||||
driver: context.mounts[""],
|
||||
};
|
||||
};
|
||||
|
||||
const getMounts = (base: string, includeParent: boolean) => {
|
||||
return context.mountpoints
|
||||
.filter(mountpoint => (mountpoint.startsWith(base)) || (includeParent && base!.startsWith(mountpoint)))
|
||||
.map(mountpoint => ({
|
||||
relativeBase: base.length > mountpoint.length ? base!.slice(mountpoint.length) : undefined,
|
||||
.filter(
|
||||
(mountpoint) =>
|
||||
mountpoint.startsWith(base) ||
|
||||
(includeParent && base!.startsWith(mountpoint))
|
||||
)
|
||||
.map((mountpoint) => ({
|
||||
relativeBase:
|
||||
base.length > mountpoint.length
|
||||
? base!.slice(mountpoint.length)
|
||||
: undefined,
|
||||
mountpoint,
|
||||
driver: context.mounts[mountpoint]
|
||||
driver: context.mounts[mountpoint],
|
||||
}));
|
||||
};
|
||||
|
||||
const onChange: WatchCallback = (event, key) => {
|
||||
if (!context.watching) { return; }
|
||||
if (!context.watching) {
|
||||
return;
|
||||
}
|
||||
key = normalizeKey(key);
|
||||
for (const listener of context.watchListeners) {
|
||||
listener(event, key);
|
||||
@@ -59,15 +75,23 @@ export function createStorage (options: CreateStorageOptions = {}): Storage {
|
||||
};
|
||||
|
||||
const startWatch = async () => {
|
||||
if (context.watching) { return; }
|
||||
if (context.watching) {
|
||||
return;
|
||||
}
|
||||
context.watching = true;
|
||||
for (const mountpoint in context.mounts) {
|
||||
context.unwatch[mountpoint] = await watch(context.mounts[mountpoint], onChange, mountpoint);
|
||||
context.unwatch[mountpoint] = await watch(
|
||||
context.mounts[mountpoint],
|
||||
onChange,
|
||||
mountpoint
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const stopWatch = async () => {
|
||||
if (!context.watching) { return; }
|
||||
if (!context.watching) {
|
||||
return;
|
||||
}
|
||||
for (const mountpoint in context.unwatch) {
|
||||
await context.unwatch[mountpoint]();
|
||||
}
|
||||
@@ -77,17 +101,19 @@ export function createStorage (options: CreateStorageOptions = {}): Storage {
|
||||
|
||||
const storage: Storage = {
|
||||
// Item
|
||||
hasItem (key) {
|
||||
hasItem(key) {
|
||||
key = normalizeKey(key);
|
||||
const { relativeKey, driver } = getMount(key);
|
||||
return asyncCall(driver.hasItem, relativeKey);
|
||||
},
|
||||
getItem (key) {
|
||||
getItem(key) {
|
||||
key = normalizeKey(key);
|
||||
const { relativeKey, driver } = getMount(key);
|
||||
return asyncCall(driver.getItem, relativeKey).then(value => destr(value));
|
||||
return asyncCall(driver.getItem, relativeKey).then((value) =>
|
||||
destr(value)
|
||||
);
|
||||
},
|
||||
async setItem (key, value) {
|
||||
async setItem(key, value) {
|
||||
if (value === undefined) {
|
||||
return storage.removeItem(key);
|
||||
}
|
||||
@@ -101,7 +127,7 @@ export function createStorage (options: CreateStorageOptions = {}): Storage {
|
||||
onChange("update", key);
|
||||
}
|
||||
},
|
||||
async removeItem (key, removeMeta = true) {
|
||||
async removeItem(key, removeMeta = true) {
|
||||
key = normalizeKey(key);
|
||||
const { relativeKey, driver } = getMount(key);
|
||||
if (!driver.removeItem) {
|
||||
@@ -116,7 +142,7 @@ export function createStorage (options: CreateStorageOptions = {}): Storage {
|
||||
}
|
||||
},
|
||||
// Meta
|
||||
async getMeta (key, nativeMetaOnly) {
|
||||
async getMeta(key, nativeMetaOnly) {
|
||||
key = normalizeKey(key);
|
||||
const { relativeKey, driver } = getMount(key);
|
||||
const meta = Object.create(null);
|
||||
@@ -124,80 +150,95 @@ export function createStorage (options: CreateStorageOptions = {}): Storage {
|
||||
Object.assign(meta, await asyncCall(driver.getMeta, relativeKey));
|
||||
}
|
||||
if (!nativeMetaOnly) {
|
||||
const value = await asyncCall(driver.getItem, relativeKey + "$").then(value_ => destr(value_));
|
||||
const value = await asyncCall(driver.getItem, relativeKey + "$").then(
|
||||
(value_) => destr(value_)
|
||||
);
|
||||
if (value && typeof value === "object") {
|
||||
// TODO: Support date by destr?
|
||||
if (typeof value.atime === "string") { value.atime = new Date(value.atime); }
|
||||
if (typeof value.mtime === "string") { value.mtime = new Date(value.mtime); }
|
||||
if (typeof value.atime === "string") {
|
||||
value.atime = new Date(value.atime);
|
||||
}
|
||||
if (typeof value.mtime === "string") {
|
||||
value.mtime = new Date(value.mtime);
|
||||
}
|
||||
Object.assign(meta, value);
|
||||
}
|
||||
}
|
||||
return meta;
|
||||
},
|
||||
setMeta (key: string, value: any) {
|
||||
setMeta(key: string, value: any) {
|
||||
return this.setItem(key + "$", value);
|
||||
},
|
||||
removeMeta (key: string) {
|
||||
removeMeta(key: string) {
|
||||
return this.removeItem(key + "$");
|
||||
},
|
||||
// Keys
|
||||
async getKeys (base) {
|
||||
async getKeys(base) {
|
||||
base = normalizeBaseKey(base);
|
||||
const mounts = getMounts(base, true);
|
||||
let maskedMounts = [];
|
||||
const allKeys = [];
|
||||
for (const mount of mounts) {
|
||||
const rawKeys = await asyncCall(mount.driver.getKeys, mount.relativeBase);
|
||||
const rawKeys = await asyncCall(
|
||||
mount.driver.getKeys,
|
||||
mount.relativeBase
|
||||
);
|
||||
const keys = rawKeys
|
||||
.map(key => mount.mountpoint + normalizeKey(key))
|
||||
.filter(key => !maskedMounts.some(p => key.startsWith(p)));
|
||||
.map((key) => mount.mountpoint + normalizeKey(key))
|
||||
.filter((key) => !maskedMounts.some((p) => key.startsWith(p)));
|
||||
allKeys.push(...keys);
|
||||
|
||||
// When /mnt/foo is processed, any key in /mnt with /mnt/foo prefix should be masked
|
||||
// Using filter to improve performance. /mnt mask already covers /mnt/foo
|
||||
maskedMounts = [
|
||||
mount.mountpoint,
|
||||
...maskedMounts.filter(p => !p.startsWith(mount.mountpoint))
|
||||
...maskedMounts.filter((p) => !p.startsWith(mount.mountpoint)),
|
||||
];
|
||||
}
|
||||
return base
|
||||
? allKeys.filter(key => key.startsWith(base!) && !key.endsWith("$"))
|
||||
: allKeys.filter(key => !key.endsWith("$"));
|
||||
? allKeys.filter((key) => key.startsWith(base!) && !key.endsWith("$"))
|
||||
: allKeys.filter((key) => !key.endsWith("$"));
|
||||
},
|
||||
// Utils
|
||||
async clear (base) {
|
||||
async clear(base) {
|
||||
base = normalizeBaseKey(base);
|
||||
await Promise.all(getMounts(base, false).map(async (m) => {
|
||||
if (m.driver.clear) {
|
||||
return asyncCall(m.driver.clear);
|
||||
}
|
||||
// Fallback to remove all keys if clear not implemented
|
||||
if (m.driver.removeItem) {
|
||||
const keys = await m.driver.getKeys();
|
||||
return Promise.all(keys.map(key => m.driver.removeItem!(key)));
|
||||
}
|
||||
// Readonly
|
||||
}));
|
||||
await Promise.all(
|
||||
getMounts(base, false).map(async (m) => {
|
||||
if (m.driver.clear) {
|
||||
return asyncCall(m.driver.clear);
|
||||
}
|
||||
// Fallback to remove all keys if clear not implemented
|
||||
if (m.driver.removeItem) {
|
||||
const keys = await m.driver.getKeys();
|
||||
return Promise.all(keys.map((key) => m.driver.removeItem!(key)));
|
||||
}
|
||||
// Readonly
|
||||
})
|
||||
);
|
||||
},
|
||||
async dispose () {
|
||||
await Promise.all(Object.values(context.mounts).map(driver => dispose(driver)));
|
||||
async dispose() {
|
||||
await Promise.all(
|
||||
Object.values(context.mounts).map((driver) => dispose(driver))
|
||||
);
|
||||
},
|
||||
async watch (callback) {
|
||||
async watch(callback) {
|
||||
await startWatch();
|
||||
context.watchListeners.push(callback);
|
||||
return async () => {
|
||||
context.watchListeners = context.watchListeners.filter(listener => listener !== callback);
|
||||
context.watchListeners = context.watchListeners.filter(
|
||||
(listener) => listener !== callback
|
||||
);
|
||||
if (context.watchListeners.length === 0) {
|
||||
await stopWatch();
|
||||
}
|
||||
};
|
||||
},
|
||||
async unwatch () {
|
||||
async unwatch() {
|
||||
context.watchListeners = [];
|
||||
await stopWatch();
|
||||
},
|
||||
// Mount
|
||||
mount (base, driver) {
|
||||
mount(base, driver) {
|
||||
base = normalizeBaseKey(base);
|
||||
if (base && context.mounts[base]) {
|
||||
throw new Error(`already mounted at ${base}`);
|
||||
@@ -216,7 +257,7 @@ export function createStorage (options: CreateStorageOptions = {}): Storage {
|
||||
}
|
||||
return storage;
|
||||
},
|
||||
async unmount (base: string, _dispose = true) {
|
||||
async unmount(base: string, _dispose = true) {
|
||||
base = normalizeBaseKey(base);
|
||||
if (!base /* root */ || !context.mounts[base]) {
|
||||
return;
|
||||
@@ -228,36 +269,49 @@ export function createStorage (options: CreateStorageOptions = {}): Storage {
|
||||
if (_dispose) {
|
||||
await dispose(context.mounts[base]);
|
||||
}
|
||||
context.mountpoints = context.mountpoints.filter(key => key !== base);
|
||||
context.mountpoints = context.mountpoints.filter((key) => key !== base);
|
||||
delete context.mounts[base];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
export type Snapshot<T=string> = Record<string, T>
|
||||
export type Snapshot<T = string> = Record<string, T>;
|
||||
|
||||
export async function snapshot (storage: Storage, base: string): Promise<Snapshot<string>> {
|
||||
export async function snapshot(
|
||||
storage: Storage,
|
||||
base: string
|
||||
): Promise<Snapshot<string>> {
|
||||
base = normalizeBaseKey(base);
|
||||
const keys = await storage.getKeys(base);
|
||||
const snapshot: any = {};
|
||||
await Promise.all(keys.map(async (key) => {
|
||||
snapshot[key.slice(base.length)] = await storage.getItem(key);
|
||||
}));
|
||||
await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
snapshot[key.slice(base.length)] = await storage.getItem(key);
|
||||
})
|
||||
);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
export async function restoreSnapshot (driver: Storage, snapshot: Snapshot<StorageValue>, base: string = "") {
|
||||
export async function restoreSnapshot(
|
||||
driver: Storage,
|
||||
snapshot: Snapshot<StorageValue>,
|
||||
base = ""
|
||||
) {
|
||||
base = normalizeBaseKey(base);
|
||||
await Promise.all(Object.entries(snapshot).map(e => driver.setItem(base + e[0], e[1])));
|
||||
await Promise.all(
|
||||
Object.entries(snapshot).map((e) => driver.setItem(base + e[0], e[1]))
|
||||
);
|
||||
}
|
||||
|
||||
function watch (driver: Driver, onChange: WatchCallback, base: string) {
|
||||
return driver.watch ? driver.watch((event, key) => onChange(event, base + key)) : () => {};
|
||||
function watch(driver: Driver, onChange: WatchCallback, base: string) {
|
||||
return driver.watch
|
||||
? driver.watch((event, key) => onChange(event, base + key))
|
||||
: () => {};
|
||||
}
|
||||
|
||||
async function dispose (driver: Driver) {
|
||||
async function dispose(driver: Driver) {
|
||||
if (typeof driver.dispose === "function") {
|
||||
await asyncCall(driver.dispose);
|
||||
}
|
||||
|
||||
62
src/types.ts
62
src/types.ts
@@ -1,47 +1,47 @@
|
||||
export type StorageValue = null | string | String | number | Number | boolean | Boolean | object
|
||||
export type WatchEvent = "update" | "remove"
|
||||
export type WatchCallback = (event: WatchEvent, key: string) => any
|
||||
export type StorageValue = null | string | number | boolean | object;
|
||||
export type WatchEvent = "update" | "remove";
|
||||
export type WatchCallback = (event: WatchEvent, key: string) => any;
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
export type Unwatch = () => MaybePromise<void>
|
||||
export type Unwatch = () => MaybePromise<void>;
|
||||
|
||||
export interface StorageMeta {
|
||||
atime?: Date
|
||||
mtime?: Date
|
||||
[key: string]: StorageValue | Date | undefined
|
||||
atime?: Date;
|
||||
mtime?: Date;
|
||||
[key: string]: StorageValue | Date | undefined;
|
||||
}
|
||||
|
||||
export interface Driver {
|
||||
hasItem: (key: string) => MaybePromise<boolean>
|
||||
getItem: (key: string) => StorageValue
|
||||
setItem?: (key: string, value: string) => MaybePromise<void>
|
||||
removeItem?: (key: string) => MaybePromise<void>
|
||||
getMeta?: (key: string) => MaybePromise<StorageMeta>
|
||||
getKeys: (base?: string) => MaybePromise<string[]>
|
||||
clear?: () => MaybePromise<void>
|
||||
dispose?: () => MaybePromise<void>
|
||||
watch?: (callback: WatchCallback) => MaybePromise<Unwatch>
|
||||
hasItem: (key: string) => MaybePromise<boolean>;
|
||||
getItem: (key: string) => StorageValue;
|
||||
setItem?: (key: string, value: string) => MaybePromise<void>;
|
||||
removeItem?: (key: string) => MaybePromise<void>;
|
||||
getMeta?: (key: string) => MaybePromise<StorageMeta>;
|
||||
getKeys: (base?: string) => MaybePromise<string[]>;
|
||||
clear?: () => MaybePromise<void>;
|
||||
dispose?: () => MaybePromise<void>;
|
||||
watch?: (callback: WatchCallback) => MaybePromise<Unwatch>;
|
||||
}
|
||||
|
||||
export interface Storage {
|
||||
// Item
|
||||
hasItem: (key: string) => Promise<boolean>
|
||||
getItem: (key: string) => Promise<StorageValue>
|
||||
setItem: (key: string, value: StorageValue) => Promise<void>
|
||||
removeItem: (key: string, removeMeta?: boolean) => Promise<void>
|
||||
hasItem: (key: string) => Promise<boolean>;
|
||||
getItem: (key: string) => Promise<StorageValue>;
|
||||
setItem: (key: string, value: StorageValue) => Promise<void>;
|
||||
removeItem: (key: string, removeMeta?: boolean) => Promise<void>;
|
||||
// Meta
|
||||
getMeta: (key: string, nativeMetaOnly?: true) => MaybePromise<StorageMeta>
|
||||
setMeta: (key: string, value: StorageMeta) => Promise<void>
|
||||
removeMeta: (key: string) => Promise<void>
|
||||
getMeta: (key: string, nativeMetaOnly?: true) => MaybePromise<StorageMeta>;
|
||||
setMeta: (key: string, value: StorageMeta) => Promise<void>;
|
||||
removeMeta: (key: string) => Promise<void>;
|
||||
// Keys
|
||||
getKeys: (base?: string) => Promise<string[]>
|
||||
getKeys: (base?: string) => Promise<string[]>;
|
||||
// Utils
|
||||
clear: (base?: string) => Promise<void>
|
||||
dispose: () => Promise<void>
|
||||
watch: (callback: WatchCallback) => Promise<Unwatch>
|
||||
unwatch: () => Promise<void>
|
||||
clear: (base?: string) => Promise<void>;
|
||||
dispose: () => Promise<void>;
|
||||
watch: (callback: WatchCallback) => Promise<Unwatch>;
|
||||
unwatch: () => Promise<void>;
|
||||
// Mount
|
||||
mount: (base: string, driver: Driver) => Storage
|
||||
unmount: (base: string, dispose?: boolean) => Promise<void>
|
||||
mount: (base: string, driver: Driver) => Storage;
|
||||
unmount: (base: string, dispose?: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
28
src/utils.ts
28
src/utils.ts
@@ -1,6 +1,6 @@
|
||||
import type { Storage } from "./types";
|
||||
|
||||
type StorageKeys = Array<keyof Storage>
|
||||
type StorageKeys = Array<keyof Storage>;
|
||||
|
||||
const storageKeyProperties: StorageKeys = [
|
||||
"hasItem",
|
||||
@@ -13,38 +13,42 @@ const storageKeyProperties: StorageKeys = [
|
||||
"getKeys",
|
||||
"clear",
|
||||
"mount",
|
||||
"unmount"
|
||||
"unmount",
|
||||
];
|
||||
|
||||
export function prefixStorage (storage: Storage, base: string) {
|
||||
export function prefixStorage(storage: Storage, base: string) {
|
||||
base = normalizeBaseKey(base);
|
||||
if (!base) {
|
||||
return storage;
|
||||
}
|
||||
const nsStorage: Storage = { ...storage };
|
||||
for (const property of storageKeyProperties) {
|
||||
// @ts-ignore Better types?
|
||||
nsStorage[property] = (key: string = "", ...arguments_) => storage[property](base + key, ...arguments_);
|
||||
// @ts-ignore
|
||||
nsStorage[property] = (key = "", ...args) =>
|
||||
// @ts-ignore
|
||||
storage[property](base + key, ...args);
|
||||
}
|
||||
nsStorage.getKeys = (key: string = "", ...arguments_) =>
|
||||
nsStorage.getKeys = (key = "", ...arguments_) =>
|
||||
storage
|
||||
.getKeys(base + key, ...arguments_)
|
||||
// Remove Prefix
|
||||
.then(keys => keys.map(key => key.slice(base.length)));
|
||||
.then((keys) => keys.map((key) => key.slice(base.length)));
|
||||
|
||||
return nsStorage;
|
||||
}
|
||||
|
||||
export function normalizeKey (key?: string) {
|
||||
if (!key) { return ""; }
|
||||
export function normalizeKey(key?: string) {
|
||||
if (!key) {
|
||||
return "";
|
||||
}
|
||||
return key.replace(/[/\\]/g, ":").replace(/:+/g, ":").replace(/^:|:$/g, "");
|
||||
}
|
||||
|
||||
export function joinKeys (...keys: string[]) {
|
||||
export function joinKeys(...keys: string[]) {
|
||||
return normalizeKey(keys.join(":"));
|
||||
}
|
||||
|
||||
export function normalizeBaseKey (base?: string) {
|
||||
export function normalizeBaseKey(base?: string) {
|
||||
base = normalizeKey(base);
|
||||
return base ? (base + ":") : "";
|
||||
return base ? base + ":" : "";
|
||||
}
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
import { describe } from 'vitest'
|
||||
import { createStorage } from '../../src'
|
||||
import CloudflareKVBinding from '../../src/drivers/cloudflare-kv-binding'
|
||||
import { testDriver } from './utils'
|
||||
import { describe } from "vitest";
|
||||
import { createStorage } from "../../src";
|
||||
import CloudflareKVBinding from "../../src/drivers/cloudflare-kv-binding";
|
||||
import { testDriver } from "./utils";
|
||||
|
||||
const mockStorage = createStorage()
|
||||
const mockStorage = createStorage();
|
||||
|
||||
// https://developers.cloudflare.com/workers/runtime-apis/kv/
|
||||
const mockBinding: KVNamespace = {
|
||||
get(key) { return mockStorage.getItem(key) as any },
|
||||
getWithMetadata(key: string) {return mockStorage.getItem(key) as any },
|
||||
put(key, value) { return mockStorage.setItem(key, value) as any },
|
||||
delete(key) { return mockStorage.removeItem(key) as any },
|
||||
list(opts) { return mockStorage.getKeys(opts?.prefix || undefined).then(keys => ({ keys: keys.map(name => ({ name })) })) as any },
|
||||
}
|
||||
get(key) {
|
||||
return mockStorage.getItem(key) as any;
|
||||
},
|
||||
getWithMetadata(key: string) {
|
||||
return mockStorage.getItem(key) as any;
|
||||
},
|
||||
put(key, value) {
|
||||
return mockStorage.setItem(key, value) as any;
|
||||
},
|
||||
delete(key) {
|
||||
return mockStorage.removeItem(key) as any;
|
||||
},
|
||||
list(opts) {
|
||||
return mockStorage
|
||||
.getKeys(opts?.prefix || undefined)
|
||||
.then((keys) => ({ keys: keys.map((name) => ({ name })) })) as any;
|
||||
},
|
||||
};
|
||||
|
||||
describe('drivers: cloudflare-kv', () => {
|
||||
describe("drivers: cloudflare-kv", () => {
|
||||
testDriver({
|
||||
driver: CloudflareKVBinding({ binding: mockBinding })
|
||||
})
|
||||
})
|
||||
driver: CloudflareKVBinding({ binding: mockBinding }),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,50 +1,54 @@
|
||||
import { afterAll, beforeAll, describe } from 'vitest'
|
||||
import driver, { KVHTTPOptions } from '../../src/drivers/cloudflare-kv-http'
|
||||
import { testDriver } from './utils'
|
||||
import { rest } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { afterAll, beforeAll, describe } from "vitest";
|
||||
import driver, { KVHTTPOptions } from "../../src/drivers/cloudflare-kv-http";
|
||||
import { testDriver } from "./utils";
|
||||
import { rest } from "msw";
|
||||
import { setupServer } from "msw/node";
|
||||
|
||||
const baseURL =
|
||||
'https://api.cloudflare.com/client/v4/accounts/:accountId/storage/kv/namespaces/:namespaceId'
|
||||
"https://api.cloudflare.com/client/v4/accounts/:accountId/storage/kv/namespaces/:namespaceId";
|
||||
|
||||
const store: Record<string, any> = {}
|
||||
const store: Record<string, any> = {};
|
||||
|
||||
const server = setupServer(
|
||||
rest.get(`${baseURL}/values/:key`, (req, res, ctx) => {
|
||||
const key = req.params.key as string
|
||||
const key = req.params.key as string;
|
||||
if (!(key in store)) {
|
||||
return res(ctx.status(404), ctx.json(null))
|
||||
return res(ctx.status(404), ctx.json(null));
|
||||
}
|
||||
return res(ctx.status(200), ctx.set('content-type', 'application/octet-stream'), ctx.json(store[key]))
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.set("content-type", "application/octet-stream"),
|
||||
ctx.json(store[key])
|
||||
);
|
||||
}),
|
||||
|
||||
rest.get(`${baseURL}/metadata/:key`, (req, res, ctx) => {
|
||||
const key = req.params.key as string
|
||||
const key = req.params.key as string;
|
||||
if (!(key in store)) {
|
||||
return res(ctx.status(404), ctx.json({ success: false }))
|
||||
return res(ctx.status(404), ctx.json({ success: false }));
|
||||
}
|
||||
return res(ctx.status(200), ctx.json({ success: true }))
|
||||
return res(ctx.status(200), ctx.json({ success: true }));
|
||||
}),
|
||||
|
||||
rest.put(`${baseURL}/values/:key`, (req, res, ctx) => {
|
||||
const key = req.params.key as string
|
||||
store[key] = req.body
|
||||
return res(ctx.status(204), ctx.json(null))
|
||||
const key = req.params.key as string;
|
||||
store[key] = req.body;
|
||||
return res(ctx.status(204), ctx.json(null));
|
||||
}),
|
||||
|
||||
rest.delete(`${baseURL}/values/:key`, (req, res, ctx) => {
|
||||
const key = req.params.key as string
|
||||
delete store[key]
|
||||
return res(ctx.status(204))
|
||||
const key = req.params.key as string;
|
||||
delete store[key];
|
||||
return res(ctx.status(204));
|
||||
}),
|
||||
|
||||
rest.get(`${baseURL}/keys`, (req, res, ctx) => {
|
||||
const prefix = req.url.searchParams.get('prefix') || ''
|
||||
let keys = Object.keys(store)
|
||||
if (req.url.searchParams.has('prefix')) {
|
||||
keys = keys.filter((key) => key.startsWith(prefix))
|
||||
const prefix = req.url.searchParams.get("prefix") || "";
|
||||
let keys = Object.keys(store);
|
||||
if (req.url.searchParams.has("prefix")) {
|
||||
keys = keys.filter((key) => key.startsWith(prefix));
|
||||
}
|
||||
const result = keys.map((key) => ({ name: key }))
|
||||
const result = keys.map((key) => ({ name: key }));
|
||||
|
||||
const data = {
|
||||
result,
|
||||
@@ -53,37 +57,37 @@ const server = setupServer(
|
||||
messages: [],
|
||||
result_info: {
|
||||
count: keys.length,
|
||||
cursor: '',
|
||||
cursor: "",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
return res(ctx.status(200), ctx.json(data))
|
||||
return res(ctx.status(200), ctx.json(data));
|
||||
}),
|
||||
|
||||
rest.delete(`${baseURL}/bulk`, (_req, res, ctx) => {
|
||||
Object.keys(store).forEach((key) => delete store[key])
|
||||
return res(ctx.status(204))
|
||||
Object.keys(store).forEach((key) => delete store[key]);
|
||||
return res(ctx.status(204));
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const mockOptions: KVHTTPOptions = {
|
||||
apiToken: 'api-token',
|
||||
accountId: 'account-id',
|
||||
namespaceId: 'namespace-id',
|
||||
}
|
||||
apiToken: "api-token",
|
||||
accountId: "account-id",
|
||||
namespaceId: "namespace-id",
|
||||
};
|
||||
|
||||
describe('drivers: cloudflare-kv-http', () => {
|
||||
describe("drivers: cloudflare-kv-http", () => {
|
||||
beforeAll(() => {
|
||||
// Establish requests interception layer before all tests.
|
||||
server.listen()
|
||||
})
|
||||
server.listen();
|
||||
});
|
||||
afterAll(() => {
|
||||
// Clean up after all tests are done, preventing this
|
||||
// interception layer from affecting irrelevant tests.
|
||||
server.close()
|
||||
})
|
||||
server.close();
|
||||
});
|
||||
|
||||
testDriver({
|
||||
driver: driver(mockOptions),
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,49 +1,43 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { resolve } from 'path'
|
||||
import { readFile, writeFile } from '../../src/drivers/utils/node-fs'
|
||||
import { testDriver } from './utils'
|
||||
import driver from '../../src/drivers/fs'
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { resolve } from "path";
|
||||
import { readFile, writeFile } from "../../src/drivers/utils/node-fs";
|
||||
import { testDriver } from "./utils";
|
||||
import driver from "../../src/drivers/fs";
|
||||
|
||||
describe('drivers: fs', () => {
|
||||
const dir = resolve(__dirname, 'tmp')
|
||||
describe("drivers: fs", () => {
|
||||
const dir = resolve(__dirname, "tmp");
|
||||
|
||||
testDriver({
|
||||
driver: driver({ base: dir }),
|
||||
additionalTests(ctx) {
|
||||
it('check filesystem', async () => {
|
||||
expect(await readFile(resolve(dir, 's1/a'))).toBe('test_data')
|
||||
})
|
||||
it('native meta', async () => {
|
||||
const meta = await ctx.storage.getMeta('/s1/a')
|
||||
expect(meta.atime?.constructor.name).toBe('Date')
|
||||
expect(meta.mtime?.constructor.name).toBe('Date')
|
||||
expect(meta.size).toBeGreaterThan(0)
|
||||
})
|
||||
it('watch filesystem', async () => {
|
||||
const watcher = vi.fn()
|
||||
await ctx.storage.watch(watcher)
|
||||
await writeFile(resolve(dir, 's1/random_file'), 'random')
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
expect(watcher).toHaveBeenCalledWith('update', 's1:random_file')
|
||||
})
|
||||
it("check filesystem", async () => {
|
||||
expect(await readFile(resolve(dir, "s1/a"))).toBe("test_data");
|
||||
});
|
||||
it("native meta", async () => {
|
||||
const meta = await ctx.storage.getMeta("/s1/a");
|
||||
expect(meta.atime?.constructor.name).toBe("Date");
|
||||
expect(meta.mtime?.constructor.name).toBe("Date");
|
||||
expect(meta.size).toBeGreaterThan(0);
|
||||
});
|
||||
it("watch filesystem", async () => {
|
||||
const watcher = vi.fn();
|
||||
await ctx.storage.watch(watcher);
|
||||
await writeFile(resolve(dir, "s1/random_file"), "random");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
expect(watcher).toHaveBeenCalledWith("update", "s1:random_file");
|
||||
});
|
||||
|
||||
const invalidKeys = [
|
||||
'../foobar',
|
||||
'..:foobar',
|
||||
'../',
|
||||
'..:',
|
||||
'..'
|
||||
]
|
||||
const invalidKeys = ["../foobar", "..:foobar", "../", "..:", ".."];
|
||||
for (const key of invalidKeys) {
|
||||
it('disallow path travesal: ' , async () => {
|
||||
await expect(ctx.storage.getItem(key)).rejects.toThrow('Invalid key')
|
||||
})
|
||||
it("disallow path travesal: ", async () => {
|
||||
await expect(ctx.storage.getItem(key)).rejects.toThrow("Invalid key");
|
||||
});
|
||||
}
|
||||
|
||||
it('allow double dots in filename: ' , async () => {
|
||||
await ctx.storage.setItem('s1/te..st..js', 'ok')
|
||||
expect(await ctx.storage.getItem('s1/te..st..js')).toBe('ok')
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
it("allow double dots in filename: ", async () => {
|
||||
await ctx.storage.setItem("s1/te..st..js", "ok");
|
||||
expect(await ctx.storage.getItem("s1/te..st..js")).toBe("ok");
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import driver from '../../src/drivers/http'
|
||||
import { createStorage } from '../../src'
|
||||
import { createStorageServer } from '../../src/server'
|
||||
import { listen } from 'listhen'
|
||||
import { describe, it, expect } from "vitest";
|
||||
import driver from "../../src/drivers/http";
|
||||
import { createStorage } from "../../src";
|
||||
import { createStorageServer } from "../../src/server";
|
||||
import { listen } from "listhen";
|
||||
|
||||
describe('drivers: http', () => {
|
||||
it('basic', async () => {
|
||||
const storage = createStorage()
|
||||
const server = createStorageServer(storage)
|
||||
describe("drivers: http", () => {
|
||||
it("basic", async () => {
|
||||
const storage = createStorage();
|
||||
const server = createStorageServer(storage);
|
||||
|
||||
const { url, close } = await listen(server.handle, {
|
||||
port: { random: true }
|
||||
})
|
||||
storage.mount('/http', driver({ base: url }))
|
||||
port: { random: true },
|
||||
});
|
||||
storage.mount("/http", driver({ base: url }));
|
||||
|
||||
expect(await storage.hasItem('/http/foo')).toBe(false)
|
||||
expect(await storage.hasItem("/http/foo")).toBe(false);
|
||||
|
||||
await storage.setItem('/http/foo', 'bar')
|
||||
expect(await storage.getItem('http:foo')).toBe('bar')
|
||||
expect(await storage.hasItem('/http/foo')).toBe(true)
|
||||
await storage.setItem("/http/foo", "bar");
|
||||
expect(await storage.getItem("http:foo")).toBe("bar");
|
||||
expect(await storage.hasItem("/http/foo")).toBe(true);
|
||||
|
||||
const date = new Date()
|
||||
await storage.setMeta('/http/foo', { mtime: date })
|
||||
const date = new Date();
|
||||
await storage.setMeta("/http/foo", { mtime: date });
|
||||
|
||||
expect(await storage.getMeta('/http/foo')).toMatchObject({ mtime: date, status: 200 })
|
||||
expect(await storage.getMeta("/http/foo")).toMatchObject({
|
||||
mtime: date,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
await close()
|
||||
})
|
||||
})
|
||||
await close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import driver from '../../src/drivers/localstorage'
|
||||
import { testDriver } from './utils'
|
||||
import { JSDOM } from 'jsdom'
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import driver from "../../src/drivers/localstorage";
|
||||
import { testDriver } from "./utils";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
describe('drivers: localstorage', () => {
|
||||
const jsdom = new JSDOM('', {
|
||||
url: 'http://localhost'
|
||||
})
|
||||
jsdom.virtualConsole.sendTo(console)
|
||||
describe("drivers: localstorage", () => {
|
||||
const jsdom = new JSDOM("", {
|
||||
url: "http://localhost",
|
||||
});
|
||||
jsdom.virtualConsole.sendTo(console);
|
||||
|
||||
testDriver({
|
||||
driver: driver({ window: jsdom.window as unknown as typeof window }),
|
||||
additionalTests: (ctx => {
|
||||
it('check localstorage', () => {
|
||||
expect(jsdom.window.localStorage.getItem('s1:a')).toBe('test_data')
|
||||
})
|
||||
it('watch localstorage', async () => {
|
||||
const watcher = vi.fn()
|
||||
await ctx.storage.watch(watcher)
|
||||
additionalTests: (ctx) => {
|
||||
it("check localstorage", () => {
|
||||
expect(jsdom.window.localStorage.getItem("s1:a")).toBe("test_data");
|
||||
});
|
||||
it("watch localstorage", async () => {
|
||||
const watcher = vi.fn();
|
||||
await ctx.storage.watch(watcher);
|
||||
|
||||
// Emulate
|
||||
// jsdom.window.localStorage.setItem('s1:random_file', 'random')
|
||||
const ev = jsdom.window.document.createEvent('CustomEvent')
|
||||
ev.initEvent('storage', true)
|
||||
const ev = jsdom.window.document.createEvent("CustomEvent");
|
||||
ev.initEvent("storage", true);
|
||||
// @ts-ignore
|
||||
ev.key = 's1:random_file'
|
||||
ev.key = "s1:random_file";
|
||||
// @ts-ignore
|
||||
ev.newValue = 'random'
|
||||
jsdom.window.dispatchEvent(ev)
|
||||
ev.newValue = "random";
|
||||
jsdom.window.dispatchEvent(ev);
|
||||
|
||||
expect(watcher).toHaveBeenCalledWith('update', 's1:random_file')
|
||||
})
|
||||
it('unwatch localstorage', async () => {
|
||||
const watcher = vi.fn()
|
||||
const unwatch = await ctx.storage.watch(watcher)
|
||||
expect(watcher).toHaveBeenCalledWith("update", "s1:random_file");
|
||||
});
|
||||
it("unwatch localstorage", async () => {
|
||||
const watcher = vi.fn();
|
||||
const unwatch = await ctx.storage.watch(watcher);
|
||||
|
||||
// Emulate
|
||||
// jsdom.window.localStorage.setItem('s1:random_file', 'random')
|
||||
const ev = jsdom.window.document.createEvent('CustomEvent')
|
||||
ev.initEvent('storage', true)
|
||||
const ev = jsdom.window.document.createEvent("CustomEvent");
|
||||
ev.initEvent("storage", true);
|
||||
// @ts-ignore
|
||||
ev.key = 's1:random_file'
|
||||
ev.key = "s1:random_file";
|
||||
// @ts-ignore
|
||||
ev.newValue = 'random'
|
||||
const ev2 = jsdom.window.document.createEvent('CustomEvent')
|
||||
ev2.initEvent('storage', true)
|
||||
ev.newValue = "random";
|
||||
const ev2 = jsdom.window.document.createEvent("CustomEvent");
|
||||
ev2.initEvent("storage", true);
|
||||
// @ts-ignore
|
||||
ev2.key = 's1:random_file2'
|
||||
ev2.key = "s1:random_file2";
|
||||
// @ts-ignore
|
||||
ev2.newValue = 'random'
|
||||
|
||||
jsdom.window.dispatchEvent(ev)
|
||||
ev2.newValue = "random";
|
||||
|
||||
await unwatch()
|
||||
|
||||
jsdom.window.dispatchEvent(ev2)
|
||||
jsdom.window.dispatchEvent(ev);
|
||||
|
||||
expect(watcher).toHaveBeenCalledWith('update', 's1:random_file')
|
||||
expect(watcher).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
await unwatch();
|
||||
|
||||
jsdom.window.dispatchEvent(ev2);
|
||||
|
||||
expect(watcher).toHaveBeenCalledWith("update", "s1:random_file");
|
||||
expect(watcher).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe } from 'vitest'
|
||||
import driver from '../../src/drivers/memory'
|
||||
import { testDriver } from './utils'
|
||||
import { describe } from "vitest";
|
||||
import driver from "../../src/drivers/memory";
|
||||
import { testDriver } from "./utils";
|
||||
|
||||
describe('drivers: memory', () => {
|
||||
describe("drivers: memory", () => {
|
||||
testDriver({
|
||||
driver: driver()
|
||||
})
|
||||
})
|
||||
driver: driver(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe } from 'vitest'
|
||||
import driver from '../../src/drivers/overlay'
|
||||
import memory from '../../src/drivers/memory'
|
||||
import { testDriver } from './utils'
|
||||
import { describe } from "vitest";
|
||||
import driver from "../../src/drivers/overlay";
|
||||
import memory from "../../src/drivers/memory";
|
||||
import { testDriver } from "./utils";
|
||||
|
||||
describe('drivers: overlay', () => {
|
||||
const [s1, s2] = [memory(), memory()]
|
||||
describe("drivers: overlay", () => {
|
||||
const [s1, s2] = [memory(), memory()];
|
||||
testDriver({
|
||||
driver: driver({
|
||||
layers: [s1, s2]
|
||||
})
|
||||
})
|
||||
})
|
||||
layers: [s1, s2],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,73 +1,79 @@
|
||||
import { it, expect } from 'vitest'
|
||||
import { Storage, Driver, createStorage, restoreSnapshot } from '../../src'
|
||||
import { it, expect } from "vitest";
|
||||
import { Storage, Driver, createStorage, restoreSnapshot } from "../../src";
|
||||
|
||||
export interface TestContext {
|
||||
storage: Storage
|
||||
driver: Driver
|
||||
storage: Storage;
|
||||
driver: Driver;
|
||||
}
|
||||
|
||||
export interface TestOptions {
|
||||
driver: Driver
|
||||
additionalTests?: (ctx: TestContext) => void
|
||||
driver: Driver;
|
||||
additionalTests?: (ctx: TestContext) => void;
|
||||
}
|
||||
|
||||
export function testDriver (opts: TestOptions) {
|
||||
export function testDriver(opts: TestOptions) {
|
||||
const ctx: TestContext = {
|
||||
storage: createStorage(),
|
||||
driver: opts.driver
|
||||
}
|
||||
driver: opts.driver,
|
||||
};
|
||||
|
||||
it('init', async () => {
|
||||
ctx.storage = createStorage({ driver: opts.driver })
|
||||
await restoreSnapshot(ctx.storage, { initial: 'works' })
|
||||
expect(await ctx.storage.getItem('initial')).toBe('works')
|
||||
await ctx.storage.clear()
|
||||
})
|
||||
it("init", async () => {
|
||||
ctx.storage = createStorage({ driver: opts.driver });
|
||||
await restoreSnapshot(ctx.storage, { initial: "works" });
|
||||
expect(await ctx.storage.getItem("initial")).toBe("works");
|
||||
await ctx.storage.clear();
|
||||
});
|
||||
|
||||
it('initial state', async () => {
|
||||
expect(await ctx.storage.hasItem('s1:a')).toBe(false)
|
||||
expect(await ctx.storage.getItem('s2:a')).toBe(null)
|
||||
expect(await ctx.storage.getKeys()).toMatchObject([])
|
||||
})
|
||||
it("initial state", async () => {
|
||||
expect(await ctx.storage.hasItem("s1:a")).toBe(false);
|
||||
expect(await ctx.storage.getItem("s2:a")).toBe(null);
|
||||
expect(await ctx.storage.getKeys()).toMatchObject([]);
|
||||
});
|
||||
|
||||
it('setItem', async () => {
|
||||
await ctx.storage.setItem('s1:a', 'test_data')
|
||||
await ctx.storage.setItem('s2:a', 'test_data')
|
||||
expect(await ctx.storage.hasItem('s1:a')).toBe(true)
|
||||
expect(await ctx.storage.getItem('s1:a')).toBe('test_data')
|
||||
})
|
||||
it("setItem", async () => {
|
||||
await ctx.storage.setItem("s1:a", "test_data");
|
||||
await ctx.storage.setItem("s2:a", "test_data");
|
||||
expect(await ctx.storage.hasItem("s1:a")).toBe(true);
|
||||
expect(await ctx.storage.getItem("s1:a")).toBe("test_data");
|
||||
});
|
||||
|
||||
it('getKeys', async () => {
|
||||
expect(await ctx.storage.getKeys().then(k => k.sort())).toMatchObject(['s1:a', 's2:a'].sort())
|
||||
expect(await ctx.storage.getKeys('s1').then(k => k.sort())).toMatchObject(['s1:a'].sort())
|
||||
})
|
||||
it("getKeys", async () => {
|
||||
expect(await ctx.storage.getKeys().then((k) => k.sort())).toMatchObject(
|
||||
["s1:a", "s2:a"].sort()
|
||||
);
|
||||
expect(await ctx.storage.getKeys("s1").then((k) => k.sort())).toMatchObject(
|
||||
["s1:a"].sort()
|
||||
);
|
||||
});
|
||||
|
||||
it('serialize (object)', async () => {
|
||||
await ctx.storage.setItem('/data/test.json', { json: 'works' })
|
||||
expect(await ctx.storage.getItem('/data/test.json')).toMatchObject({ json: 'works' })
|
||||
})
|
||||
it("serialize (object)", async () => {
|
||||
await ctx.storage.setItem("/data/test.json", { json: "works" });
|
||||
expect(await ctx.storage.getItem("/data/test.json")).toMatchObject({
|
||||
json: "works",
|
||||
});
|
||||
});
|
||||
|
||||
it('serialize (primitive)', async () => {
|
||||
await ctx.storage.setItem('/data/true.json', true)
|
||||
expect(await ctx.storage.getItem('/data/true.json')).toBe(true)
|
||||
})
|
||||
it("serialize (primitive)", async () => {
|
||||
await ctx.storage.setItem("/data/true.json", true);
|
||||
expect(await ctx.storage.getItem("/data/true.json")).toBe(true);
|
||||
});
|
||||
|
||||
if (opts.additionalTests) {
|
||||
opts.additionalTests(ctx)
|
||||
opts.additionalTests(ctx);
|
||||
}
|
||||
|
||||
it('removeItem', async () => {
|
||||
await ctx.storage.removeItem('s1:a')
|
||||
expect(await ctx.storage.hasItem('s1:a')).toBe(false)
|
||||
expect(await ctx.storage.getItem('s1:a')).toBe(null)
|
||||
})
|
||||
it("removeItem", async () => {
|
||||
await ctx.storage.removeItem("s1:a");
|
||||
expect(await ctx.storage.hasItem("s1:a")).toBe(false);
|
||||
expect(await ctx.storage.getItem("s1:a")).toBe(null);
|
||||
});
|
||||
|
||||
it('clear', async () => {
|
||||
await ctx.storage.clear()
|
||||
expect(await ctx.storage.getKeys()).toMatchObject([])
|
||||
})
|
||||
it("clear", async () => {
|
||||
await ctx.storage.clear();
|
||||
expect(await ctx.storage.getKeys()).toMatchObject([]);
|
||||
});
|
||||
|
||||
it('dispose', async () => {
|
||||
await ctx.storage.dispose()
|
||||
})
|
||||
it("dispose", async () => {
|
||||
await ctx.storage.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@ describe("server", () => {
|
||||
const storage = createStorage();
|
||||
const storageServer = createStorageServer(storage);
|
||||
const { close, url: serverURL } = await listen(storageServer.handle, {
|
||||
port: { random: true }
|
||||
port: { random: true },
|
||||
});
|
||||
|
||||
const fetchStorage = (url: string, options?: any) => $fetch(url, { baseURL: serverURL, ...options });
|
||||
const fetchStorage = (url: string, options?: any) =>
|
||||
$fetch(url, { baseURL: serverURL, ...options });
|
||||
|
||||
expect(await fetchStorage("foo", {})).toMatchObject([]);
|
||||
|
||||
@@ -20,7 +21,9 @@ describe("server", () => {
|
||||
await storage.setMeta("foo/bar", { mtime: new Date() });
|
||||
expect(await fetchStorage("foo/bar")).toBe("bar");
|
||||
|
||||
expect(await fetchStorage("foo/bar", { method: "PUT", body: "updated" })).toBe("OK");
|
||||
expect(
|
||||
await fetchStorage("foo/bar", { method: "PUT", body: "updated" })
|
||||
).toBe("OK");
|
||||
expect(await fetchStorage("foo/bar")).toBe("updated");
|
||||
expect(await fetchStorage("/")).toMatchObject(["foo/bar"]);
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { createStorage, snapshot, restoreSnapshot, prefixStorage } from "../src";
|
||||
import {
|
||||
createStorage,
|
||||
snapshot,
|
||||
restoreSnapshot,
|
||||
prefixStorage,
|
||||
} from "../src";
|
||||
import memory from "../src/drivers/memory";
|
||||
|
||||
const data = {
|
||||
"etc:conf": "test",
|
||||
"data:foo": 123
|
||||
"data:foo": 123,
|
||||
};
|
||||
|
||||
describe("storage", () => {
|
||||
|
||||
Reference in New Issue
Block a user