Kılavuz
Başlarken
Kurulum#
npm i @thefinder/core @thefinder/react @thefinder/next
Next.js#
1. Sunucu ayarı (lib/finder.ts)
import { createTheFinder, localDriver, uploadsDir } from "@thefinder/next";
export const finder = createTheFinder({
volumes: [
{
id: "uploads",
name: "Uploads",
driver: localDriver({ root: uploadsDir() }), // <proje kökü>/uploads
url: "/uploads",
denyExtensions: ["php", "exe"],
},
],
authorize: async ({ request }) => Boolean(await getSession(request)), // isteğe bağlı
});
2. API route'u (app/api/files/route.ts)
import { createNextRoutes } from "@thefinder/next";
import { finder } from "@/lib/finder";
export const runtime = "nodejs";
export const { GET, POST } = createNextRoutes(finder);
3. Dosya sunumu (app/uploads/[...path]/route.ts)
import { createUploadsRoute } from "@thefinder/next";
export const runtime = "nodejs";
export const { GET, HEAD } = createUploadsRoute();
4. Sayfa
"use client";
import { TheFinder } from "@thefinder/react";
import "@thefinder/react/styles.css";
export default function Files() {
return (
<div style={{ height: "100vh" }}>
<TheFinder endpoint="/api/files" locale="tr" />
</div>
);
}
Standalone ve uploads klasörü#
uploadsDir()her modda proje kökündekiuploads/klasörünü verir:next devvenext start: çalışma dizini.output: "standalone":server.jskendini.next/standalone/...içine taşıdığı için.nextöncesindeki yol kullanılır. Yani dosyalar build çıktısına değil, yine proje köküne yazılır.
- Docker veya kalıcı disk kullanıyorsanız klasörü ortam değişkenleriyle değiştirebilirsiniz:
CI_FINDER_UPLOADS_DIR=/data/uploadssadece yükleme klasörünü değiştirir.CI_FINDER_ROOT=/appproje kökünü değiştirir.
- Dosyalar neden
public/altında değil? Next.js production'da, build sonrasıpublic/klasörüne eklenen dosyaları sunmaz.createUploadsRouteise dosyaları her istekte diskten okur. Range (video ileri sarma), ETag ve 304 destekler; HTML ve SVG dosyalarınıCSP: sandboxile sunar. - Standalone build
.next/staticklasörünü kendiliğinden kopyalamaz. examples/next/scripts/copy-standalone-assets.mjs bu işipostbuildadımında yapar. - Monorepo'da
next.configiçineoutputFileTracingRootekleyin; örnek: examples/next/next.config.mjs. - Küçük resimler standalone'da da çalışsın diye sharp'ın yerel kütüphanelerini pakete ekleyin. Next'in dosya izleyicisi
libvipsDLL/.sodosyalarını kendiliğinden almıyor:// next.config.mjs outputFileTracingIncludes: { "/api/files": ["./node_modules/@img/**/*"] },Bu ayar unutulsa bile dosya yöneticisi çalışmaya devam eder: sunucu günlüğe bir uyarı yazar ve küçük resim yerine orijinal görselleri gönderir.
Yetkilendirme#
theFinder oturum yönetimi yapmaz; uygulamanızdaki oturumu (Auth.js, Clerk, kendi çereziniz…) authorize hook'unda okursunuz:
import { TheFinderError } from "@thefinder/next"; // veya "@thefinder/core"
createTheFinder({
volumes,
authorize: async ({ request, cmd }) => {
const session = await getSession(request);
if (!session) throw new TheFinderError("UNAUTHORIZED"); // → 401
if (session.role === "viewer") return { readOnly: true }; // her şey salt okunur
return true; // false → 403
},
});
falsedöndürmek 403,UNAUTHORIZEDfırlatmak 401 verir.{ readOnly: true }döndürülürse o istek için bütün volume'ler salt okunur olur. Arayüz yazma eylemlerini kendiliğinden gizler, API de yazma istekleriniREAD_ONLYhatasıyla reddeder.- Arayüz 401 alınca
onUnauthorizedçağrılır, örneğin<TheFinder onUnauthorized={() => location.assign("/login")} />. /uploadsadresini de korumak isterseniz:createUploadsRoute({ authorize: (request) => isSignedIn(request) }).
Bun#
import { createTheFinder, createFileServer } from "@thefinder/core";
import { localDriver } from "@thefinder/core/local";
import index from "./index.html";
const driver = localDriver({ root: "./uploads" });
const finder = createTheFinder({ volumes: [{ id: "files", driver, url: "/uploads" }] });
Bun.serve({
routes: {
"/": index, // React arayüzünü Bun kendisi paketler
"/api/files": finder.handler,
"/uploads/*": createFileServer({ driver, prefix: "/uploads" }),
},
});
Node.js, Express, Fastify, Koa#
import { toExpress, toNodeHandler, toFastify, toKoa } from "@thefinder/core/node";
app.all("/api/files", toExpress(finder.handler)); // Express / Connect
http.createServer(toNodeHandler(finder.handler)); // düz node:http
fastify.register(toFastify(finder.handler), { prefix: "/api/files" }); // Fastify
router.all("/api/files", toKoa(finder.handler)); // Koa
express.json() ve koa-bodyparser gibi gövde ayrıştırıcılar sorun çıkarmaz; gövde önceden okunmuşsa adaptör onu yeniden oluşturur. Adaptörler Express 5, Fastify 5 ve Koa 3 üzerinde gerçek sunucuyla test edildi. Testlerde JSON komutları, parça parça yükleme, Range indirme ve zip indirme denendi. Hono, Deno ve Cloudflare gibi Web standardı ortamlarda finder.handler doğrudan kullanılır.
S3, R2, MinIO#
import { s3Driver } from "@thefinder/core/s3";
volumes: [
{ id: "local", driver: localDriver({ root: uploadsDir() }), url: "/uploads" },
{
id: "s3",
name: "Bulut",
driver: s3Driver({
bucket: "my-bucket",
region: "auto", // R2 için "auto"
endpoint: "https://<id>.r2.cloudflarestorage.com", // AWS için boş bırakın
accessKeyId: process.env.S3_KEY!,
secretAccessKey: process.env.S3_SECRET!,
prefix: "uploads/",
}),
url: "https://cdn.example.com/uploads", // public bucket/CDN varsa; yoksa presigned URL kullanılır
},
];
- AWS SDK kullanılmaz; imzalama (SigV4) Web Crypto ile yapılır ve Edge runtime'da da çalışır.
- Yerel disk ile S3 arasında kopyalama ve taşıma desteklenir.
- Yüklemeler S3 multipart olarak yapılır ve sunucu istekler arasında durum tutmaz, bu yüzden serverless ortamda da çalışır. S3 kullanılıyorsa
chunkSizeen az 5 MiB olmalı (varsayılan 5 MiB). - Görsel editörü S3'teki görselleri düzenleyebilsin diye bucket'ta CORS ayarı gerekir (
GET, uygulamanızın origin'i). - Gerçek bir S3 uyumlu sunucuya karşı entegrasyon testi:
CI_FINDER_S3_ENDPOINT=http://127.0.0.1:7070 CI_FINDER_S3_KEY=... CI_FINDER_S3_SECRET=... npm run test:s3 -w @thefinder/coreBu testler Versity Gateway 1.8 üzerinde çalıştırıldı ve hepsi geçti. İmza doğrulaması, Türkçe ve özel karakterli anahtarlar, multipart yükleme, 1.000'den fazla nesnenin sayfalı listelenmesi, presigned URL, çöp kutusu ve küçük resimler kontrol edildi.
Küçük resimler#
import { sharpThumbnailer } from "@thefinder/core/sharp";
createTheFinder({
volumes,
thumbnails: { generator: sharpThumbnailer() }, // sizes: [128, 256, 512], concurrency: 2
});
- Üretim için
sharpkullanılır. Next.js projelerinde zaten kurulu gelir (next/imageonu kullanır); diğer projelerdenpm i sharpyeterli. Core paketinin kendisi sharp'a bağımlı değildir, sadece@thefinder/core/sharpalt yolu onu içeri alır. - İlk istekte üretilir, volume içindeki gizli
.tf-thumbs/klasöründe saklanır (S3'te de çalışır). Kaynak görsel değişince yeniden üretilir; dosya silinince, taşınınca veya adı değişince ilgili küçük resim temizlenir. Klasör istendiği zaman silinebilir, gerektiğinde yeniden oluşur. - Kötüye kullanıma karşı koruma:
- Yalnızca belirlenen boyutlar üretilir, keyfi boyut istenemez.
- Aynı anda yapılan üretim sayısı sınırlıdır ve aynı görsel için gelen eşzamanlı istekler tek üretimde birleştirilir.
- 40 MB'tan büyük dosyalar ve 120 megapikselden büyük görseller küçük resme çevrilmez.
- Biçimi desteklenmeyen ya da bozuk bir dosyada orijinal gönderilir; arayüz yine düzgün çalışır. sharp hiç yüklenemiyorsa (örneğin yerel ikili dosyalar sunucuya kopyalanmamışsa) API çalışmaya devam eder, günlüğe bir kez uyarı düşer ve orijinal görseller gösterilir.
- Küçük resimler
?v=<mtime>içeren adreslerle sunulduğu için tarayıcı onları 1 yıl önbellekte tutar.
Çöp kutusu#
Veritabanı kullanılmaz. Her volume'ün çöpü kendi içinde, gizli .tf-trash/ klasöründe tutulur:
uploads/.tf-trash/
mgh2k1-a8f3c2d1/ ← silinen öğe (asıl adıyla)
Faturalar/...
mgh2k1-a8f3c2d1.json ← { name, originalPath, deletedAt, kind, size }
- Yerel diskte de S3'te de aynı şekilde çalışır ve sunucu yeniden başlasa da korunur.
- Geri yüklenen öğe eski yerine döner. Eski klasör silinmişse yeniden oluşturulur; aynı adla başka bir öğe varsa adı "dosya (2)" olur.
- Süresi dolan öğeler çöp kutusu her listelendiğinde temizlenir, bunun için cron gerekmez.
.tf-trashnormal listede, aramada, zip indirmede ve/uploadsroute'unda hiç görünmez;showHidden: trueolsa bile.
{ id: "uploads", driver, trash: { retentionDays: 14 } } // varsayılan: 30 gün
{ id: "tmp", driver, trash: false } // çöp kutusu yok, doğrudan silinir
API komutları: rm (varsayılan olarak çöpe taşır, permanent: true ile kalıcı siler), trash, restore, purge ({ ids } ya da { all: true }).
Sürüm geçmişi#
Çöp kutusu gibi veritabanı kullanmaz. Bir dosyanın üzerine yazılmadan önce eski içeriği volume içindeki gizli .tf-versions/ klasörüne kopyalanır:
uploads/.tf-versions/
3f0a…c9/ ← sha1(dosya yolu)
file.json ← { path }
mgh2k1-a8f3c2-edit.bin ← zaman-rastgele-sebep
mgh3p0-19bd04-optimize.bin
- Sürüm alınan durumlar: editörde kaydetme (kod ve görsel), toplu optimizasyonda üzerine yazma, "değiştir" ile yükleme, yapıştırmada "değiştir", bir sürümü geri yükleme (geri yüklemeden önceki hali de sürüm olur, yani geri alınabilir).
- Yeniden adlandırma ve taşımada (klasör taşıma dahil) geçmiş dosyayla birlikte gider. Dosya çöpe gidince geçmiş kalır, çöpten geri gelince yine bağlı olur; kalıcı silinince geçmiş de silinir.
- theFinder dışında silinen bir dosyanın geçmişinden sürüm geri yüklenirse dosya yeniden oluşturulur.
- Sürüm almak bir kopyalamadır: yerel diskte dosya kopyası, S3'te sunucu tarafı
CopyObject(veri sunucudan geçmez). Bir dosyanın geçmişini listelemek tek bir klasör listeleme isteğidir.
{ id: "uploads", driver, versions: { maxPerFile: 20, retentionDays: 0 } } // varsayılan; 0 = süresiz
{ id: "tmp", driver, versions: false }
Arayüzde: sağ tık → "Sürüm geçmişi", ayrıntılar paneli ve görsel editöründeki geçmiş düğmesi. Toplu temizlik depolama panelinden yapılır: X günden eski sürümler, dosya başına son N sürüm, silinmiş dosyaların sürümleri, tümü.
API komutları: versions, version (GET, sürümü sunar), revert, rmVersions, stats, cleanup ({ target: "versions", mode: "all" | "orphaned" | "older" | "keep" } ya da { target: "cache" }).
Toplu görsel işlemleri#
import { sharpImages, sharpThumbnailer } from "@thefinder/core/sharp";
createTheFinder({
volumes,
thumbnails: { generator: sharpThumbnailer() },
images: sharpImages(), // yeniden boyutlandırma, sıkıştırma, WebP / AVIF / JPEG / PNG
});
- Görselleri seçip sağ tık → "Görselleri optimize et…": hazır boyutlar (3840, 2560, 1920, 1280, 800) ya da özel boyut, biçim, kalite.
- Görsel en-boy oranı korunarak kutuya sığdırılır, asla büyütülmez. EXIF yönü uygulanır, meta veriler temizlenir; animasyonlu GIF/WebP, hedef biçim destekliyorsa animasyonlu kalır.
- Biçim değişirse (örn. PNG → WebP) orijinal dosya korunur ve yanına
foto.webpoluşturulur. Biçim aynıysa üzerine yazılır (eski hali sürüm geçmişine girer) ya da-optimizedkopyası oluşturulur. - "Küçülmeyen görselleri atla" açıkken, sonucu orijinalden büyük çıkan dosyaya dokunulmaz. PNG'de kalite 100'ün altındaysa 256 renkli palete indirilir.
- Aynı anda en fazla iki görsel işlenir; 60 MB'tan büyük girdiler reddedilir (
maxImageSize). imagesverilmezse aynı pencere görselleri tarayıcıda (canvas ile) işler: PNG, JPEG ve WebP.
Dosya seçici: useFilePicker#
theFinder'ı modal bir seçici olarak açar ve seçilen dosyaları Promise ile döndürür. Vazgeçilirse null gelir.
import { useFilePicker } from "@thefinder/react";
const picker = useFilePicker({ endpoint: "/api/files", accept: "image/*" });
<input value={cover} onChange={(e) => setCover(e.target.value)} />
<button onClick={async () => {
const files = await picker.open();
if (files) setCover(files[0].url); // "/uploads/kapak.png"
}}>Dosya seç</button>
accept:<input type="file">ile aynı söz dizimi ("image/*",".pdf,.docx") ya da(entry) => boolean. Uymayan dosyalar soluk görünür, seçilemez.multiple,selectLabel,locale,themeve diğer<TheFinder />prop'ları geçerlidir.absoluteUrls: truetam adres döndürür.- React dışında (vanilla JS, Vue…) aynı şey:
const files = await openFilePicker({ endpoint: "/api/files" }). popup: trueseçiciyi elFinder gibi ayrı bir tarayıcı penceresinde açar;openFinderWindow({ endpoint })aynısını dosya yöneticisinin tamamı için yapar. Ayrıca bir sayfa servis etmek gerekmez.- Çalışan örnek:
examples/nextiçindeki/playgroundsayfası.
CKEditor#
@thefinder/ckeditor, CKFinder'ın yaptığı işi yapar: araç çubuğuna dosya yöneticisi düğmesi ekler ve yapıştırılan / sürüklenen görselleri theFinder üzerinden yükler.
CKEditor 5
import { TheFinder } from "@thefinder/ckeditor";
ClassicEditor.create(el, {
licenseKey: "GPL",
plugins: [Essentials, Paragraph, Image, ImageUpload, Link, TheFinder],
toolbar: ["bold", "link", "|", "theFinder"],
theFinder: { endpoint: "/api/files", uploadFolder: "/editor" },
});
CKEditor 4
import { registerTheFinder } from "@thefinder/ckeditor/v4";
registerTheFinder(window.CKEDITOR);
CKEDITOR.replace("body", {
extraPlugins: "thefinder,uploadimage",
theFinder: { endpoint: "/api/files", uploadFolder: "/editor" },
});
- Seçilen görseller görsel olarak, diğer dosyalar bağlantı olarak eklenir (seçili metin varsa ona bağlantı verilir).
- CKEditor 4'te Resim ve Bağlantı pencerelerindeki "Sunucuyu Gözat" düğmeleri de theFinder'ı açar.
uploadFolderyoksa oluşturulur;falseverilirse editörün kendi yükleme ayarı kullanılır.pickerile seçicinin ayarları (accept,theme…),openPickerile tamamen kendi seçiciniz verilebilir.- CKEditor 4'ün açık kaynak son sürümü 4.22.1'dir; 4.23 ve sonrası ticari lisans anahtarı ister. Connector iki sürümle de çalışır.
<TheFinder /> prop'ları#
| Prop | Açıklama |
|---|---|
endpoint | API adresi, örn. /api/files |
headers, credentials | Ek istek başlıkları (örn. Authorization), cross-origin için cookie ayarı |
locale, messages | "tr" / "en", çeviri ekleme veya ezme |
theme | "auto" (varsayılan), "light", "dark" |
skin | "classic" (varsayılan), "macos" (Finder benzeri görünüm) |
density | "comfortable" (varsayılan), "compact" |
settings | Başlıktaki ayarlar menüsü (tema, görünüm, yoğunluk). Kullanıcının seçimi persistKey altında saklanır. Varsayılan true |
height, className, style | Boyut ve stil. Varsayılan yükseklik ebeveynin %100'ü |
initialFolder, defaultView, persistKey | Başlangıç klasörü, varsayılan görünüm ve sıralama, tercihlerin saklanacağı localStorage anahtarı |
rememberFolder | Kullanıcının en son açtığı klasörü tekrar açar (varsayılan: true) |
onSelect, selectLabel, multiple | Seçici modu (örn. CMS'te "görsel seç" alanı) |
accept, onCancel | Seçici modunda seçilebilecek dosyalar ("image/*", ".pdf"…) ve "Vazgeç" düğmesi |
onOpen | Dosya açılışını yakalar; true döndürürseniz varsayılan davranış çalışmaz |
onChange | Her değişiklikten sonra çağrılır (yükleme, silme, taşıma…) |
editors | "Birlikte aç" menüsüne kendi editörünüzü ekler |
Tema#
Tüm stiller @layer thefinder içindedir, yani katman dışında yazdığınız her kural onları ezer. Renkler CSS değişkenleriyle tanımlı:
.tf-root {
--tf-accent: #0f766e;
--tf-radius: 4px;
--tf-font: "Inter", system-ui, sans-serif;
}
.tf-root[data-theme="dark"] {
--tf-bg: #101418;
}
Güvenlik#
- Path traversal:
..içeren yollar çözülmeye çalışılmaz, doğrudan reddedilir. Kök dışına işaret eden symlink'ler gizlenir. - CSRF: Tüm değişiklik isteklerinde
x-thefinderbaşlığı zorunludur; değişiklik yapan komutlar GET ile çalışmaz. - Yetkilendirme: Volume bazında
readOnly,permission(action, path), uzantı izin ve yasak listeleri,maxUploadSize; komut bazındaauthorizehook'u. - Arşivler: Zip-slip ve zip bombası koruması (
maxExtractSize). - İç klasörler:
.tf-trash,.tf-versionsve.tf-thumbshiçbir komutla, aramayla veya/uploadsadresiyle erişilemez; bu adla klasör de oluşturulamaz. - Aktif içerik: Yüklenen HTML ve SVG dosyaları sandbox CSP ile sunulur, uygulamanızın origin'inde script çalıştıramaz.
- Kimlik doğrulama: Kimlik doğrulama
authorizehook'unda yapılır; varsayılan ayarlarla API'ye herkes erişebilir. Ayrıntılar için Yetkilendirme bölümüne bakın.
Geliştirme#
npm install
npm run build # core → next → react → ckeditor
npm test # core testleri (Node)
npm run test:bun # aynı testler Bun ile
npm run dev # paketleri derler ve Next örneğini başlatır (http://localhost:3000)
npm run site:dev # GitHub Pages sitesini derler ve sunar (http://localhost:4173); önce npm run build
Örnekler: examples/next, examples/bun, examples/express.
Site kaynağı site/ klasöründedir. Belgeler sayfaları README.md, README.tr.md ve docs/api/**/*.md dosyalarından üretilir; canlı demo gerçek @thefinder/core motorunu tarayıcıda, bellek içi bir sürücüyle çalıştırır. main'e gelen her push'ta Pages iş akışı siteyi yayımlar.
Sürüm ve yayın#
Sürümler Changesets ile yönetilir. Dört paket her zaman aynı sürüm numarasıyla yayımlanır.
- Yayımlanacak bir değişiklik yaptığınızda
npx changesetçalıştırın. Etkilenen paketleri vepatch/minor/majorseçimini yapıp kısa bir açıklama yazın. Oluşan.changeset/*.mddosyasını değişiklikle birlikte commit'leyin. main'e gelen her push'ta Release iş akışı çalışır. Bekleyen changeset varsa sürümleri yükselten veCHANGELOG.mddosyalarını yazan bir "chore: version packages" PR'ı açar ya da günceller.- O PR birleştirildiğinde paketler derlenir ve npm'e yayımlanır. Yayınlar provenance bilgisiyle gider.
Gereksinimler:
- npm'de
thefinderorganizasyonu (@thefinder/*kapsamı için). - Repo secret'ı olarak
NPM_TOKEN: yayın yetkili, granular ya da Automation türünde bir npm token'ı. - Repo ayarlarında Settings › Actions › General › Allow GitHub Actions to create and approve pull requests açık olmalı.
Elle yayın da mümkündür: npm run version-packages, ardından npm login ve npm run release.
Lisans#
MIT © Alper Koşay
Bu sayfa README.tr.md dosyasından üretildi.