mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 14:20:35 +08:00
Integrate Authority Blob storage
This commit is contained in:
329
maintenance/authority-blob-adapter.js
Normal file
329
maintenance/authority-blob-adapter.js
Normal file
@@ -0,0 +1,329 @@
|
||||
import { normalizeAuthorityBaseUrl } from "../runtime/authority-capabilities.js";
|
||||
|
||||
export const AUTHORITY_BLOB_ENDPOINT = "/v1/blob";
|
||||
|
||||
function toPlainData(value, fallbackValue = null) {
|
||||
if (value == null) return fallbackValue;
|
||||
if (typeof globalThis.structuredClone === "function") {
|
||||
try {
|
||||
return globalThis.structuredClone(value);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
} catch {
|
||||
return fallbackValue;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRecordId(value) {
|
||||
return String(value ?? "").trim();
|
||||
}
|
||||
|
||||
function normalizeInteger(value, fallback = 0, min = 0, max = Number.MAX_SAFE_INTEGER) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return Math.min(max, Math.max(min, Math.trunc(parsed)));
|
||||
}
|
||||
|
||||
function decodeBase64Utf8(base64Text = "") {
|
||||
const normalizedBase64 = String(base64Text ?? "");
|
||||
if (!normalizedBase64) return "";
|
||||
if (typeof globalThis.atob === "function" && typeof globalThis.TextDecoder === "function") {
|
||||
const binary = globalThis.atob(normalizedBase64);
|
||||
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
if (typeof Buffer !== "undefined") {
|
||||
return Buffer.from(normalizedBase64, "base64").toString("utf8");
|
||||
}
|
||||
return normalizedBase64;
|
||||
}
|
||||
|
||||
function tryParseJsonText(text, fallbackValue = null) {
|
||||
if (typeof text !== "string") return fallbackValue;
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return fallbackValue;
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeAuthorityBlobPath(path = "") {
|
||||
const normalized = String(path ?? "")
|
||||
.trim()
|
||||
.replace(/\\/g, "/")
|
||||
.replace(/^authority:\/\/private\//i, "")
|
||||
.replace(/^\/+/, "")
|
||||
.replace(/\/+/g, "/");
|
||||
return normalized.replace(/\/+$/g, "");
|
||||
}
|
||||
|
||||
function normalizeBlobPayload(result = null) {
|
||||
if (!result || typeof result !== "object" || Array.isArray(result)) return result;
|
||||
const source = result.file || result.blob || result.result || result;
|
||||
if (source.payload !== undefined) return source.payload;
|
||||
if (source.json !== undefined) return source.json;
|
||||
if (source.value !== undefined) return source.value;
|
||||
if (source.data !== undefined) {
|
||||
if (source.encoding === "base64" || source.base64 === true) {
|
||||
return tryParseJsonText(decodeBase64Utf8(source.data), source.data);
|
||||
}
|
||||
if (typeof source.data === "string") {
|
||||
return tryParseJsonText(source.data, source.data);
|
||||
}
|
||||
return source.data;
|
||||
}
|
||||
if (source.content !== undefined) {
|
||||
if (typeof source.content === "string") {
|
||||
return tryParseJsonText(source.content, source.content);
|
||||
}
|
||||
return source.content;
|
||||
}
|
||||
if (source.body !== undefined) {
|
||||
if (typeof source.body === "string") {
|
||||
return tryParseJsonText(source.body, source.body);
|
||||
}
|
||||
return source.body;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeBlobRecordSource(input = null) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
||||
return input.file || input.blob || input.result || input;
|
||||
}
|
||||
|
||||
export function normalizeAuthorityBlobReadResult(input = null, fallbackPath = "") {
|
||||
const source = normalizeBlobRecordSource(input);
|
||||
const path = normalizeAuthorityBlobPath(source.path || source.name || fallbackPath);
|
||||
const missing =
|
||||
source.exists === false ||
|
||||
source.found === false ||
|
||||
source.missing === true ||
|
||||
Number(source.status || source.statusCode || 0) === 404;
|
||||
if (missing) {
|
||||
return {
|
||||
exists: false,
|
||||
path,
|
||||
payload: null,
|
||||
contentType: String(source.contentType || source.type || ""),
|
||||
raw: toPlainData(input, input),
|
||||
};
|
||||
}
|
||||
return {
|
||||
exists: input != null && source.ok !== false,
|
||||
path,
|
||||
payload: normalizeBlobPayload(input),
|
||||
contentType: String(source.contentType || source.type || "application/json"),
|
||||
etag: String(source.etag || source.hash || ""),
|
||||
updatedAt: source.updatedAt || source.updated_at || source.lastModified || "",
|
||||
raw: toPlainData(input, input),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeAuthorityBlobWriteResult(input = null, fallbackPath = "") {
|
||||
const source = normalizeBlobRecordSource(input);
|
||||
const path = normalizeAuthorityBlobPath(source.path || source.name || fallbackPath);
|
||||
return {
|
||||
ok: input == null ? true : source.ok !== false && source.error == null,
|
||||
path,
|
||||
url: String(source.url || source.href || ""),
|
||||
size: normalizeInteger(source.size || source.bytes, 0, 0),
|
||||
etag: String(source.etag || source.hash || ""),
|
||||
updatedAt: source.updatedAt || source.updated_at || source.lastModified || "",
|
||||
raw: toPlainData(input, input),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeAuthorityBlobDeleteResult(input = null, fallbackPath = "") {
|
||||
const source = normalizeBlobRecordSource(input);
|
||||
const path = normalizeAuthorityBlobPath(source.path || source.name || fallbackPath);
|
||||
const missing =
|
||||
source.exists === false ||
|
||||
source.found === false ||
|
||||
source.missing === true ||
|
||||
Number(source.status || source.statusCode || 0) === 404;
|
||||
return {
|
||||
ok: input == null ? true : source.ok !== false,
|
||||
deleted: missing ? false : source.deleted !== false && source.ok !== false,
|
||||
missing,
|
||||
path,
|
||||
raw: toPlainData(input, input),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeAuthorityBlobConfig(settings = {}, overrides = {}) {
|
||||
const source = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
|
||||
return {
|
||||
baseUrl: normalizeAuthorityBaseUrl(source.authorityBaseUrl ?? source.baseUrl),
|
||||
enabled:
|
||||
source.authorityBlobCheckpointEnabled !== false &&
|
||||
source.blobCheckpointEnabled !== false &&
|
||||
source.authorityBlobEnabled !== false,
|
||||
failOpen: source.authorityFailOpen !== false && source.failOpen !== false,
|
||||
namespace: normalizeRecordId(source.authorityBlobNamespace || source.blobNamespace || "st-bme"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export class AuthorityBlobHttpClient {
|
||||
constructor(options = {}) {
|
||||
this.baseUrl = normalizeAuthorityBaseUrl(options.baseUrl);
|
||||
this.fetchImpl = options.fetchImpl || (typeof fetch === "function" ? fetch.bind(globalThis) : null);
|
||||
this.headerProvider = typeof options.headerProvider === "function" ? options.headerProvider : null;
|
||||
}
|
||||
|
||||
async request(action, payload = {}) {
|
||||
if (typeof this.fetchImpl !== "function") {
|
||||
throw new Error("Authority Blob fetch unavailable");
|
||||
}
|
||||
const response = await this.fetchImpl(`${this.baseUrl}${AUTHORITY_BLOB_ENDPOINT}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
...(this.headerProvider ? this.headerProvider() || {} : {}),
|
||||
},
|
||||
body: JSON.stringify({ action, ...payload }),
|
||||
});
|
||||
if (!response?.ok) {
|
||||
const text = await response?.text?.().catch(() => "");
|
||||
throw new Error(text || `Authority Blob HTTP ${response?.status || "unknown"}`);
|
||||
}
|
||||
return await response.json().catch(() => ({}));
|
||||
}
|
||||
|
||||
async writeJson(payload = {}) {
|
||||
return await this.request("writeJson", payload);
|
||||
}
|
||||
|
||||
async writeText(payload = {}) {
|
||||
return await this.request("writeText", payload);
|
||||
}
|
||||
|
||||
async readJson(payload = {}) {
|
||||
return await this.request("readJson", payload);
|
||||
}
|
||||
|
||||
async delete(payload = {}) {
|
||||
return await this.request("delete", payload);
|
||||
}
|
||||
|
||||
async stat(payload = {}) {
|
||||
return await this.request("stat", payload);
|
||||
}
|
||||
}
|
||||
|
||||
export function createAuthorityBlobClient(config = {}, options = {}) {
|
||||
const injected = options.blobClient || config.blobClient || globalThis.__stBmeAuthorityBlobClient;
|
||||
if (injected) return injected;
|
||||
return new AuthorityBlobHttpClient({
|
||||
baseUrl: config.baseUrl,
|
||||
fetchImpl: options.fetchImpl || config.fetchImpl,
|
||||
headerProvider: options.headerProvider || config.headerProvider,
|
||||
});
|
||||
}
|
||||
|
||||
async function callClient(client, methodNames = [], action = "request", payload = {}) {
|
||||
for (const methodName of methodNames) {
|
||||
if (typeof client?.[methodName] === "function") {
|
||||
return await client[methodName](payload);
|
||||
}
|
||||
}
|
||||
if (typeof client?.request === "function") {
|
||||
return await client.request(action, payload);
|
||||
}
|
||||
if (typeof client === "function") {
|
||||
return await client({ action, ...payload });
|
||||
}
|
||||
throw new Error(`Authority Blob ${action} unavailable`);
|
||||
}
|
||||
|
||||
function throwIfAborted(signal) {
|
||||
if (signal?.aborted) {
|
||||
throw signal.reason instanceof Error
|
||||
? signal.reason
|
||||
: Object.assign(new Error("操作已终止"), { name: "AbortError" });
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthorityBlobAdapter {
|
||||
constructor(config = {}, options = {}) {
|
||||
this.config = normalizeAuthorityBlobConfig(config, options.configOverrides || {});
|
||||
this.client = createAuthorityBlobClient(this.config, options);
|
||||
}
|
||||
|
||||
async writeJson(path, payload = null, options = {}) {
|
||||
throwIfAborted(options.signal);
|
||||
const normalizedPath = normalizeAuthorityBlobPath(path);
|
||||
if (!normalizedPath) throw new Error("Authority Blob path is required");
|
||||
const result = await callClient(this.client, ["writeJson", "putJson", "writeFile", "put"], "writeJson", {
|
||||
namespace: options.namespace || this.config.namespace,
|
||||
path: normalizedPath,
|
||||
name: normalizedPath,
|
||||
contentType: options.contentType || "application/json",
|
||||
payload: toPlainData(payload, payload),
|
||||
data: toPlainData(payload, payload),
|
||||
metadata: toPlainData(options.metadata, {}),
|
||||
});
|
||||
return normalizeAuthorityBlobWriteResult(result, normalizedPath);
|
||||
}
|
||||
|
||||
async writeText(path, text = "", options = {}) {
|
||||
throwIfAborted(options.signal);
|
||||
const normalizedPath = normalizeAuthorityBlobPath(path);
|
||||
if (!normalizedPath) throw new Error("Authority Blob path is required");
|
||||
const result = await callClient(this.client, ["writeText", "writeFile", "putText", "put"], "writeText", {
|
||||
namespace: options.namespace || this.config.namespace,
|
||||
path: normalizedPath,
|
||||
name: normalizedPath,
|
||||
contentType: options.contentType || "text/plain; charset=utf-8",
|
||||
text: String(text ?? ""),
|
||||
data: String(text ?? ""),
|
||||
metadata: toPlainData(options.metadata, {}),
|
||||
});
|
||||
return normalizeAuthorityBlobWriteResult(result, normalizedPath);
|
||||
}
|
||||
|
||||
async readJson(path, options = {}) {
|
||||
throwIfAborted(options.signal);
|
||||
const normalizedPath = normalizeAuthorityBlobPath(path);
|
||||
if (!normalizedPath) return normalizeAuthorityBlobReadResult({ exists: false }, "");
|
||||
const result = await callClient(this.client, ["readJson", "getJson", "readFile", "get"], "readJson", {
|
||||
namespace: options.namespace || this.config.namespace,
|
||||
path: normalizedPath,
|
||||
name: normalizedPath,
|
||||
});
|
||||
return normalizeAuthorityBlobReadResult(result, normalizedPath);
|
||||
}
|
||||
|
||||
async delete(path, options = {}) {
|
||||
throwIfAborted(options.signal);
|
||||
const normalizedPath = normalizeAuthorityBlobPath(path);
|
||||
if (!normalizedPath) return normalizeAuthorityBlobDeleteResult({ exists: false }, "");
|
||||
const result = await callClient(this.client, ["delete", "deleteFile", "remove", "unlink"], "delete", {
|
||||
namespace: options.namespace || this.config.namespace,
|
||||
path: normalizedPath,
|
||||
name: normalizedPath,
|
||||
});
|
||||
return normalizeAuthorityBlobDeleteResult(result, normalizedPath);
|
||||
}
|
||||
|
||||
async stat(path, options = {}) {
|
||||
throwIfAborted(options.signal);
|
||||
const normalizedPath = normalizeAuthorityBlobPath(path);
|
||||
if (!normalizedPath) return normalizeAuthorityBlobReadResult({ exists: false }, "");
|
||||
const result = await callClient(this.client, ["stat", "head", "metadata"], "stat", {
|
||||
namespace: options.namespace || this.config.namespace,
|
||||
path: normalizedPath,
|
||||
name: normalizedPath,
|
||||
});
|
||||
return normalizeAuthorityBlobReadResult(result, normalizedPath);
|
||||
}
|
||||
}
|
||||
|
||||
export function createAuthorityBlobAdapter(config = {}, options = {}) {
|
||||
return new AuthorityBlobAdapter(config, options);
|
||||
}
|
||||
Reference in New Issue
Block a user