feat: Openinary medya galerisi — görsel yükleme/seçme (mucomutfak klasörü)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8974cb3e1e
commit
b3a3a4b450
@@ -0,0 +1,105 @@
|
||||
// server/media.mjs — Openinary (media.ayris.tech) proxy, "mucomutfak" klasörüne kilitli
|
||||
const BASE = () => process.env.OPENINARY_API_URL?.replace(/\/$/, "");
|
||||
const KEY = () => process.env.OPENINARY_API_KEY;
|
||||
export const MEDIA_ROOT = "mucomutfak";
|
||||
|
||||
export function mediaEnabled() {
|
||||
return Boolean(BASE() && KEY());
|
||||
}
|
||||
|
||||
function assertInsideRoot(p) {
|
||||
const clean = String(p || "").replace(/^\/+|\/+$/g, "");
|
||||
if (
|
||||
clean !== MEDIA_ROOT &&
|
||||
!clean.startsWith(`${MEDIA_ROOT}/`)
|
||||
) {
|
||||
throw Object.assign(new Error(`Yol '${MEDIA_ROOT}' klasörü içinde olmalı`), {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
if (clean.includes("..")) {
|
||||
throw Object.assign(new Error("Geçersiz yol"), { status: 400 });
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
function deliveryUrl(path, transform = "") {
|
||||
const t = transform ? `${transform}/` : "";
|
||||
return `${BASE()}/t/${t}${path}`;
|
||||
}
|
||||
|
||||
// Klasör içeriğini listele
|
||||
export async function listMedia(path = MEDIA_ROOT) {
|
||||
const clean = assertInsideRoot(path);
|
||||
const res = await fetch(
|
||||
`${BASE()}/api/storage?path=${encodeURIComponent(clean)}`,
|
||||
{ headers: { Authorization: `Bearer ${KEY()}` } },
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw Object.assign(new Error(`Openinary listeleme hatası (${res.status})`), {
|
||||
status: 502,
|
||||
});
|
||||
}
|
||||
const data = await res.json();
|
||||
const IMG_RE = /\.(webp|jpe?g|png|gif|svg|avif|heic|heif)$/i;
|
||||
return {
|
||||
path: clean,
|
||||
folders: (data.folders || []).map((f) => ({ name: f.name, path: f.path })),
|
||||
files: (data.files || [])
|
||||
.filter((f) => IMG_RE.test(f.name))
|
||||
.map((f) => ({
|
||||
name: f.name,
|
||||
path: f.path,
|
||||
size: f.size,
|
||||
mtime: f.mtime,
|
||||
url: deliveryUrl(f.path),
|
||||
thumb: deliveryUrl(f.path, "w_240,h_240,c_fill"),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// Multipart isteği olduğu gibi Openinary'ye aktar (folder alanı client FormData'sında)
|
||||
export async function proxyUpload(req) {
|
||||
const res = await fetch(`${BASE()}/api/upload`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${KEY()}`,
|
||||
"content-type": req.headers["content-type"],
|
||||
...(req.headers["content-length"]
|
||||
? { "content-length": req.headers["content-length"] }
|
||||
: {}),
|
||||
},
|
||||
body: req,
|
||||
duplex: "half",
|
||||
});
|
||||
const body = await res.json().catch(() => ({}));
|
||||
if (body?.files) {
|
||||
for (const f of body.files) {
|
||||
if (f.path) {
|
||||
f.url = deliveryUrl(f.path);
|
||||
f.thumb = deliveryUrl(f.path, "w_240,h_240,c_fill");
|
||||
}
|
||||
}
|
||||
}
|
||||
return { status: res.status, body };
|
||||
}
|
||||
|
||||
export async function deleteMedia(path) {
|
||||
const clean = assertInsideRoot(path);
|
||||
if (clean === MEDIA_ROOT) {
|
||||
throw Object.assign(new Error("Kök klasör silinemez"), { status: 400 });
|
||||
}
|
||||
const res = await fetch(
|
||||
`${BASE()}/api/storage/${clean
|
||||
.split("/")
|
||||
.map(encodeURIComponent)
|
||||
.join("/")}`,
|
||||
{ method: "DELETE", headers: { Authorization: `Bearer ${KEY()}` } },
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw Object.assign(new Error(`Silme hatası (${res.status})`), {
|
||||
status: 502,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user