mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 07:10:42 +00:00
198 lines
5.2 KiB
TypeScript
198 lines
5.2 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { safeFileURLToPath } from "../infra/local-file-access.js";
|
|
import { resolveUserPath } from "../utils.js";
|
|
import { getMediaDir, resolveMediaBufferPath } from "./store.js";
|
|
|
|
type MediaReferenceErrorCode = "invalid-path" | "path-not-allowed";
|
|
|
|
export class MediaReferenceError extends Error {
|
|
code: MediaReferenceErrorCode;
|
|
|
|
constructor(code: MediaReferenceErrorCode, message: string, options?: ErrorOptions) {
|
|
super(message, options);
|
|
this.code = code;
|
|
this.name = "MediaReferenceError";
|
|
}
|
|
}
|
|
|
|
type InboundMediaReference = {
|
|
id: string;
|
|
normalizedSource: string;
|
|
physicalPath: string;
|
|
sourceType: "uri" | "path";
|
|
};
|
|
|
|
export function normalizeMediaReferenceSource(source: string): string {
|
|
const trimmed = source.trim();
|
|
if (/^media:\/\//i.test(trimmed)) {
|
|
return trimmed;
|
|
}
|
|
return trimmed.replace(/^\s*MEDIA\s*:\s*/i, "").trim();
|
|
}
|
|
|
|
type MediaReferenceSourceInfo = {
|
|
hasScheme: boolean;
|
|
hasUnsupportedScheme: boolean;
|
|
isDataUrl: boolean;
|
|
isFileUrl: boolean;
|
|
isHttpUrl: boolean;
|
|
isMediaStoreUrl: boolean;
|
|
looksLikeWindowsDrivePath: boolean;
|
|
};
|
|
|
|
export function classifyMediaReferenceSource(
|
|
source: string,
|
|
options?: { allowDataUrl?: boolean },
|
|
): MediaReferenceSourceInfo {
|
|
const allowDataUrl = options?.allowDataUrl ?? true;
|
|
const looksLikeWindowsDrivePath = /^[a-zA-Z]:[\\/]/.test(source);
|
|
const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(source);
|
|
const isFileUrl = /^file:/i.test(source);
|
|
const isHttpUrl = /^https?:\/\//i.test(source);
|
|
const isDataUrl = /^data:/i.test(source);
|
|
const isMediaStoreUrl = /^media:\/\//i.test(source);
|
|
const hasUnsupportedScheme =
|
|
hasScheme &&
|
|
!looksLikeWindowsDrivePath &&
|
|
!isFileUrl &&
|
|
!isHttpUrl &&
|
|
!isMediaStoreUrl &&
|
|
!(allowDataUrl && isDataUrl);
|
|
return {
|
|
hasScheme,
|
|
hasUnsupportedScheme,
|
|
isDataUrl,
|
|
isFileUrl,
|
|
isHttpUrl,
|
|
isMediaStoreUrl,
|
|
looksLikeWindowsDrivePath,
|
|
};
|
|
}
|
|
|
|
function maybeLocalPathFromSource(source: string): string | null {
|
|
if (/^file:/i.test(source)) {
|
|
try {
|
|
return safeFileURLToPath(source);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
if (source.startsWith("~")) {
|
|
return resolveUserPath(source);
|
|
}
|
|
if (path.isAbsolute(source)) {
|
|
return source;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function resolvePathForContainment(candidate: string): Promise<string> {
|
|
try {
|
|
return await fs.realpath(candidate);
|
|
} catch {
|
|
return path.resolve(candidate);
|
|
}
|
|
}
|
|
|
|
async function resolveInboundMediaUri(
|
|
normalizedSource: string,
|
|
): Promise<InboundMediaReference | null> {
|
|
if (!/^media:\/\//i.test(normalizedSource)) {
|
|
return null;
|
|
}
|
|
|
|
let parsed: URL;
|
|
try {
|
|
parsed = new URL(normalizedSource);
|
|
} catch (err) {
|
|
throw new MediaReferenceError("invalid-path", `Invalid media URI: ${normalizedSource}`, {
|
|
cause: err,
|
|
});
|
|
}
|
|
|
|
if (parsed.hostname !== "inbound") {
|
|
throw new MediaReferenceError(
|
|
"path-not-allowed",
|
|
`Unsupported media URI location: ${parsed.hostname || "(missing)"}`,
|
|
);
|
|
}
|
|
|
|
let id: string;
|
|
try {
|
|
id = decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
|
|
} catch (err) {
|
|
throw new MediaReferenceError("invalid-path", `Invalid media URI: ${normalizedSource}`, {
|
|
cause: err,
|
|
});
|
|
}
|
|
|
|
if (!id || id.includes("/") || id.includes("\\")) {
|
|
throw new MediaReferenceError("invalid-path", `Invalid media URI: ${normalizedSource}`);
|
|
}
|
|
|
|
return {
|
|
id,
|
|
normalizedSource,
|
|
physicalPath: await resolveInboundMediaPath(id, normalizedSource),
|
|
sourceType: "uri",
|
|
};
|
|
}
|
|
|
|
export async function resolveInboundMediaReference(
|
|
source: string,
|
|
): Promise<InboundMediaReference | null> {
|
|
const normalizedSource = normalizeMediaReferenceSource(source);
|
|
if (!normalizedSource) {
|
|
return null;
|
|
}
|
|
|
|
const uriSource = await resolveInboundMediaUri(normalizedSource);
|
|
if (uriSource) {
|
|
return uriSource;
|
|
}
|
|
|
|
const localPath = maybeLocalPathFromSource(normalizedSource);
|
|
if (!localPath) {
|
|
return null;
|
|
}
|
|
|
|
const rawInboundDir = path.resolve(getMediaDir(), "inbound");
|
|
const rawResolvedPath = path.resolve(localPath);
|
|
const rawRel = path.relative(rawInboundDir, rawResolvedPath);
|
|
const rel =
|
|
rawRel && !rawRel.startsWith("..") && !path.isAbsolute(rawRel)
|
|
? rawRel
|
|
: path.relative(
|
|
await resolvePathForContainment(rawInboundDir),
|
|
await resolvePathForContainment(localPath),
|
|
);
|
|
if (!rel || rel.startsWith("..") || path.isAbsolute(rel) || rel.includes(path.sep)) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: rel,
|
|
normalizedSource,
|
|
physicalPath: await resolveInboundMediaPath(rel, normalizedSource),
|
|
sourceType: "path",
|
|
};
|
|
}
|
|
|
|
export async function resolveMediaReferenceLocalPath(source: string): Promise<string> {
|
|
const normalizedSource = normalizeMediaReferenceSource(source);
|
|
return (await resolveInboundMediaReference(normalizedSource))?.physicalPath ?? normalizedSource;
|
|
}
|
|
|
|
async function resolveInboundMediaPath(id: string, source: string): Promise<string> {
|
|
try {
|
|
return await resolveMediaBufferPath(id, "inbound");
|
|
} catch (err) {
|
|
throw new MediaReferenceError(
|
|
"invalid-path",
|
|
err instanceof Error ? err.message : `Invalid media reference: ${source}`,
|
|
{ cause: err },
|
|
);
|
|
}
|
|
}
|