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:
AyrisAI
2026-07-21 10:06:59 +03:00
co-authored by Claude Fable 5
parent 8974cb3e1e
commit b3a3a4b450
7 changed files with 414 additions and 2 deletions
+105
View File
@@ -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;
}