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ü#

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
  },
});

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
  },
];

Küçük resimler#

import { sharpThumbnailer } from "@thefinder/core/sharp";

createTheFinder({
  volumes,
  thumbnails: { generator: sharpThumbnailer() }, // sizes: [128, 256, 512], concurrency: 2
});

Çö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 }
{ 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
{ 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
});

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>

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" },
});

<TheFinder /> prop'ları#

PropAçıklama
endpointAPI adresi, örn. /api/files
headers, credentialsEk 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"
settingsBaş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, styleBoyut ve stil. Varsayılan yükseklik ebeveynin %100'ü
initialFolder, defaultView, persistKeyBaşlangıç klasörü, varsayılan görünüm ve sıralama, tercihlerin saklanacağı localStorage anahtarı
rememberFolderKullanıcının en son açtığı klasörü tekrar açar (varsayılan: true)
onSelect, selectLabel, multipleSeçici modu (örn. CMS'te "görsel seç" alanı)
accept, onCancelSeçici modunda seçilebilecek dosyalar ("image/*", ".pdf"…) ve "Vazgeç" düğmesi
onOpenDosya açılışını yakalar; true döndürürseniz varsayılan davranış çalışmaz
onChangeHer 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#

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.

  1. Yayımlanacak bir değişiklik yaptığınızda npx changeset çalıştırın. Etkilenen paketleri ve patch / minor / major seçimini yapıp kısa bir açıklama yazın. Oluşan .changeset/*.md dosyasını değişiklikle birlikte commit'leyin.
  2. main'e gelen her push'ta Release iş akışı çalışır. Bekleyen changeset varsa sürümleri yükselten ve CHANGELOG.md dosyalarını yazan bir "chore: version packages" PR'ı açar ya da günceller.
  3. O PR birleştirildiğinde paketler derlenir ve npm'e yayımlanır. Yayınlar provenance bilgisiyle gider.

Gereksinimler:

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.