feat: not uygulaması ve altyapısını ekle
- iOS Memos benzeri PWA ön yüz eklendi (React, Tailwind) - Express tabanlı arka uç, AnythingLLM API entegrasyonu ve senkronizasyon kuyruğu oluşturuldu - Docker, TypeScript ve proje konfigürasyonları tanımlandı
This commit is contained in:
80
backend/src/anythingllm/anythingllm.client.ts
Normal file
80
backend/src/anythingllm/anythingllm.client.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { config } from "../config.js";
|
||||
import type { AnythingLLMUploadMetadata, AnythingLLMUploadResponse } from "./anythingllm.types.js";
|
||||
|
||||
function baseHeaders(): Record<string, string> {
|
||||
return {
|
||||
Authorization: `Bearer ${config.ANYTHINGLLM_API_KEY}`
|
||||
};
|
||||
}
|
||||
|
||||
async function parseJsonResponse<T>(response: Response, label: string): Promise<T> {
|
||||
const contentType = response.headers.get("content-type") ?? "";
|
||||
if (!contentType.includes("application/json")) {
|
||||
const text = await response.text();
|
||||
throw new Error(`${label} beklenmeyen yanit: ${response.status} ${text.slice(0, 200)}`);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
export async function uploadDocument(filePath: string, metadata: AnythingLLMUploadMetadata): Promise<AnythingLLMUploadResponse> {
|
||||
const form = new FormData();
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
const fileBlob = new Blob([fileBuffer], { type: "text/markdown" });
|
||||
form.append("file", fileBlob, path.basename(filePath));
|
||||
form.append("addToWorkspaces", config.ANYTHINGLLM_WORKSPACE_SLUG);
|
||||
form.append("metadata", JSON.stringify(metadata));
|
||||
|
||||
const response = await fetch(`${config.ANYTHINGLLM_ROOT_API_URL}/v1/document/upload`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...baseHeaders()
|
||||
},
|
||||
body: form
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Upload basarisiz: ${response.status} ${text}`);
|
||||
}
|
||||
|
||||
return await parseJsonResponse<AnythingLLMUploadResponse>(response, "Upload");
|
||||
}
|
||||
|
||||
export async function updateEmbeddings(adds: string[], deletes: string[]): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${config.ANYTHINGLLM_ROOT_API_URL}/v1/workspace/${config.ANYTHINGLLM_WORKSPACE_SLUG}/update-embeddings`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
...baseHeaders(),
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ adds, deletes })
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Embeddings guncelleme basarisiz: ${response.status} ${text}`);
|
||||
}
|
||||
await parseJsonResponse(response, "Embeddings guncelleme");
|
||||
}
|
||||
|
||||
export async function removeDocuments(locations: string[]): Promise<void> {
|
||||
const response = await fetch(`${config.ANYTHINGLLM_ROOT_API_URL}/v1/system/remove-documents`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
...baseHeaders(),
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ names: locations })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Dokuman temizleme basarisiz: ${response.status} ${text}`);
|
||||
}
|
||||
await parseJsonResponse(response, "Dokuman temizleme");
|
||||
}
|
||||
14
backend/src/anythingllm/anythingllm.service.ts
Normal file
14
backend/src/anythingllm/anythingllm.service.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { AnythingLLMUploadMetadata } from "./anythingllm.types.js";
|
||||
import { removeDocuments, updateEmbeddings, uploadDocument } from "./anythingllm.client.js";
|
||||
|
||||
export async function uploadNote(filePath: string, metadata: AnythingLLMUploadMetadata) {
|
||||
return uploadDocument(filePath, metadata);
|
||||
}
|
||||
|
||||
export async function updateWorkspaceEmbeddings(adds: string[], deletes: string[]) {
|
||||
await updateEmbeddings(adds, deletes);
|
||||
}
|
||||
|
||||
export async function cleanupDocuments(locations: string[]) {
|
||||
await removeDocuments(locations);
|
||||
}
|
||||
17
backend/src/anythingllm/anythingllm.types.ts
Normal file
17
backend/src/anythingllm/anythingllm.types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type AnythingLLMUploadMetadata = {
|
||||
title: string;
|
||||
docAuthor: string;
|
||||
description: string;
|
||||
docSource: string;
|
||||
};
|
||||
|
||||
export type AnythingLLMUploadResponse = {
|
||||
documents: Array<{
|
||||
id: string;
|
||||
location: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type AnythingLLMEmbeddingsResponse = {
|
||||
success: boolean;
|
||||
};
|
||||
32
backend/src/auth/basicAuth.ts
Normal file
32
backend/src/auth/basicAuth.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { config } from "../config.js";
|
||||
|
||||
function unauthorized(res: Response): void {
|
||||
res.status(401).json({ error: "Yetkisiz" });
|
||||
}
|
||||
|
||||
export async function basicAuth(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith("Basic ")) {
|
||||
unauthorized(res);
|
||||
return;
|
||||
}
|
||||
|
||||
const encoded = authHeader.slice("Basic ".length).trim();
|
||||
const decoded = Buffer.from(encoded, "base64").toString("utf-8");
|
||||
const [username, password] = decoded.split(":");
|
||||
|
||||
if (!username || !password || username !== config.AUTH_USERNAME) {
|
||||
unauthorized(res);
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, config.AUTH_PASSWORD_HASH);
|
||||
if (!isValid) {
|
||||
unauthorized(res);
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
24
backend/src/config.ts
Normal file
24
backend/src/config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import dotenv from "dotenv";
|
||||
import path from "path";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const PORT = Number(process.env.PORT || 8080);
|
||||
const NOTES_DIR = process.env.NOTES_DIR || path.resolve("/data/notes");
|
||||
|
||||
const AUTH_USERNAME = process.env.AUTH_USERNAME || "admin";
|
||||
const AUTH_PASSWORD_HASH = process.env.AUTH_PASSWORD_HASH || "";
|
||||
|
||||
const ANYTHINGLLM_ROOT_API_URL = process.env.ANYTHINGLLM_ROOT_API_URL || "";
|
||||
const ANYTHINGLLM_API_KEY = process.env.ANYTHINGLLM_API_KEY || "";
|
||||
const ANYTHINGLLM_WORKSPACE_SLUG = process.env.ANYTHINGLLM_WORKSPACE_SLUG || "";
|
||||
|
||||
export const config = {
|
||||
PORT,
|
||||
NOTES_DIR,
|
||||
AUTH_USERNAME,
|
||||
AUTH_PASSWORD_HASH,
|
||||
ANYTHINGLLM_ROOT_API_URL,
|
||||
ANYTHINGLLM_API_KEY,
|
||||
ANYTHINGLLM_WORKSPACE_SLUG
|
||||
};
|
||||
65
backend/src/notes/notes.routes.ts
Normal file
65
backend/src/notes/notes.routes.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Router } from "express";
|
||||
import { createNote, deleteNote, getNote, listNotes, manualSync, updateNote } from "./notes.service.js";
|
||||
import type { CleanupQueue } from "../queue/queue.js";
|
||||
|
||||
export function notesRoutes(queue: CleanupQueue): Router {
|
||||
const router = Router();
|
||||
|
||||
router.get("/notes", async (_req, res) => {
|
||||
const notes = await listNotes();
|
||||
res.json(notes);
|
||||
});
|
||||
|
||||
router.get("/notes/:id", async (req, res) => {
|
||||
const note = await getNote(req.params.id);
|
||||
if (!note) {
|
||||
res.status(404).json({ error: "Not bulunamadi" });
|
||||
return;
|
||||
}
|
||||
res.json(note);
|
||||
});
|
||||
|
||||
router.post("/notes", async (req, res) => {
|
||||
const { title, content } = req.body as { title?: string; content?: string };
|
||||
if (!title) {
|
||||
res.status(400).json({ error: "Baslik gerekli" });
|
||||
return;
|
||||
}
|
||||
const note = await createNote(title, content ?? "", queue);
|
||||
res.status(201).json(note);
|
||||
});
|
||||
|
||||
router.put("/notes/:id", async (req, res) => {
|
||||
const { title, content } = req.body as { title?: string; content?: string };
|
||||
if (!title) {
|
||||
res.status(400).json({ error: "Baslik gerekli" });
|
||||
return;
|
||||
}
|
||||
const note = await updateNote(req.params.id, title, content ?? "", queue);
|
||||
if (!note) {
|
||||
res.status(404).json({ error: "Not bulunamadi" });
|
||||
return;
|
||||
}
|
||||
res.json(note);
|
||||
});
|
||||
|
||||
router.delete("/notes/:id", async (req, res) => {
|
||||
const ok = await deleteNote(req.params.id, queue);
|
||||
if (!ok) {
|
||||
res.status(404).json({ error: "Not bulunamadi" });
|
||||
return;
|
||||
}
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
router.post("/notes/:id/sync", async (req, res) => {
|
||||
const ok = await manualSync(req.params.id, queue);
|
||||
if (!ok) {
|
||||
res.status(404).json({ error: "Not bulunamadi" });
|
||||
return;
|
||||
}
|
||||
res.status(202).json({ status: "syncing" });
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
174
backend/src/notes/notes.service.ts
Normal file
174
backend/src/notes/notes.service.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import type { NoteIndexItem, NoteContent } from "./notes.types.js";
|
||||
import { readIndex, writeIndex, readNoteContent, writeNoteContent, deleteNoteFile } from "./notes.storage.js";
|
||||
import { uploadDocument, updateEmbeddings } from "../anythingllm/anythingllm.client.js";
|
||||
import { config } from "../config.js";
|
||||
import { logError } from "../utils/logger.js";
|
||||
import type { CleanupQueue } from "../queue/queue.js";
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function buildMetadata(title: string, content: string) {
|
||||
const description = content.replace(/\s+/g, " ").slice(0, 120);
|
||||
return {
|
||||
title,
|
||||
docAuthor: "Note App",
|
||||
description,
|
||||
docSource: "notes-app"
|
||||
};
|
||||
}
|
||||
|
||||
export async function listNotes(): Promise<NoteIndexItem[]> {
|
||||
const index = await readIndex();
|
||||
return index.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
||||
}
|
||||
|
||||
export async function getNote(id: string): Promise<NoteContent | null> {
|
||||
const index = await readIndex();
|
||||
const item = index.find((note) => note.id === id);
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
return readNoteContent(item.id, item.filename);
|
||||
}
|
||||
|
||||
export async function createNote(title: string, content: string, queue: CleanupQueue): Promise<NoteIndexItem> {
|
||||
const id = uuidv4();
|
||||
const filename = `${id}.md`;
|
||||
const timestamp = nowIso();
|
||||
|
||||
await writeNoteContent(filename, title, content);
|
||||
|
||||
const newItem: NoteIndexItem = {
|
||||
id,
|
||||
title,
|
||||
filename,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
sync: {
|
||||
status: "syncing"
|
||||
}
|
||||
};
|
||||
|
||||
const index = await readIndex();
|
||||
index.push(newItem);
|
||||
await writeIndex(index);
|
||||
|
||||
void syncNote(id, queue);
|
||||
return newItem;
|
||||
}
|
||||
|
||||
export async function updateNote(id: string, title: string, content: string, queue: CleanupQueue): Promise<NoteIndexItem | null> {
|
||||
const index = await readIndex();
|
||||
const item = index.find((note) => note.id === id);
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await writeNoteContent(item.filename, title, content);
|
||||
|
||||
item.title = title;
|
||||
item.updatedAt = nowIso();
|
||||
item.sync.status = "syncing";
|
||||
item.sync.lastError = undefined;
|
||||
await writeIndex(index);
|
||||
|
||||
void syncNote(id, queue);
|
||||
return item;
|
||||
}
|
||||
|
||||
export async function deleteNote(id: string, queue: CleanupQueue): Promise<boolean> {
|
||||
const index = await readIndex();
|
||||
const itemIndex = index.findIndex((note) => note.id === id);
|
||||
if (itemIndex === -1) {
|
||||
return false;
|
||||
}
|
||||
const [item] = index.splice(itemIndex, 1);
|
||||
await writeIndex(index);
|
||||
await deleteNoteFile(item.filename);
|
||||
|
||||
if (item.sync.anythingllmLocation) {
|
||||
try {
|
||||
await updateEmbeddings([], [item.sync.anythingllmLocation]);
|
||||
await queue.enqueue({
|
||||
id: item.id,
|
||||
oldLocation: item.sync.anythingllmLocation,
|
||||
attempts: 0,
|
||||
nextRunAt: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logError("Silme senkron hatasi", { error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function manualSync(id: string, queue: CleanupQueue): Promise<boolean> {
|
||||
const index = await readIndex();
|
||||
const item = index.find((note) => note.id === id);
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
item.sync.status = "syncing";
|
||||
item.sync.lastError = undefined;
|
||||
await writeIndex(index);
|
||||
|
||||
void syncNote(id, queue);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function syncNote(id: string, queue: CleanupQueue): Promise<void> {
|
||||
const index = await readIndex();
|
||||
const item = index.find((note) => note.id === id);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldLocation = item.sync.anythingllmLocation;
|
||||
|
||||
try {
|
||||
const note = await readNoteContent(item.id, item.filename);
|
||||
const metadata = buildMetadata(note.title, note.content);
|
||||
|
||||
const uploadResponse = await uploadDocument(`${config.NOTES_DIR}/${item.filename}`, metadata);
|
||||
const newDoc = uploadResponse.documents[0];
|
||||
if (!newDoc) {
|
||||
throw new Error("Upload cevabinda dokuman bulunamadi");
|
||||
}
|
||||
|
||||
await updateEmbeddings([newDoc.location], oldLocation ? [oldLocation] : []);
|
||||
|
||||
item.sync.status = "synced";
|
||||
item.sync.anythingllmLocation = newDoc.location;
|
||||
item.sync.anythingllmDocumentId = newDoc.id;
|
||||
item.sync.lastSyncedAt = nowIso();
|
||||
item.sync.cleanupPending = false;
|
||||
item.sync.lastError = undefined;
|
||||
|
||||
await writeIndex(index);
|
||||
|
||||
if (oldLocation) {
|
||||
item.sync.cleanupPending = true;
|
||||
await writeIndex(index);
|
||||
await queue.enqueue({
|
||||
id: item.id,
|
||||
oldLocation,
|
||||
attempts: 0,
|
||||
nextRunAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError("Senkronizasyon hatasi", {
|
||||
noteId: item.id,
|
||||
error: (error as Error).message,
|
||||
oldLocation: oldLocation ?? null
|
||||
});
|
||||
item.sync.status = "error";
|
||||
item.sync.lastError = (error as Error).message;
|
||||
await writeIndex(index);
|
||||
}
|
||||
}
|
||||
51
backend/src/notes/notes.storage.ts
Normal file
51
backend/src/notes/notes.storage.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import path from "path";
|
||||
import { promises as fs } from "fs";
|
||||
import { ensureDir, readJsonFile, writeJsonFile, readTextFile, writeTextFile, deleteFileIfExists } from "../utils/fileUtils.js";
|
||||
import type { NoteIndexItem, NoteContent } from "./notes.types.js";
|
||||
import { config } from "../config.js";
|
||||
|
||||
const indexFilePath = path.join(config.NOTES_DIR, "index.json");
|
||||
|
||||
export async function initNotesStorage(): Promise<void> {
|
||||
await ensureDir(config.NOTES_DIR);
|
||||
const index = await readJsonFile<NoteIndexItem[]>(indexFilePath, []);
|
||||
await writeJsonFile(indexFilePath, index);
|
||||
}
|
||||
|
||||
export async function readIndex(): Promise<NoteIndexItem[]> {
|
||||
return readJsonFile<NoteIndexItem[]>(indexFilePath, []);
|
||||
}
|
||||
|
||||
export async function writeIndex(items: NoteIndexItem[]): Promise<void> {
|
||||
await writeJsonFile(indexFilePath, items);
|
||||
}
|
||||
|
||||
export function getNoteFilePath(filename: string): string {
|
||||
return path.join(config.NOTES_DIR, filename);
|
||||
}
|
||||
|
||||
export async function readNoteContent(id: string, filename: string): Promise<NoteContent> {
|
||||
const raw = await readTextFile(getNoteFilePath(filename));
|
||||
const [firstLine, ...rest] = raw.split("\n");
|
||||
const title = firstLine.replace(/^#\s*/, "").trim();
|
||||
const content = rest.join("\n").trimStart();
|
||||
return { id, title, content };
|
||||
}
|
||||
|
||||
export async function writeNoteContent(filename: string, title: string, content: string): Promise<void> {
|
||||
const body = `# ${title}\n\n${content}`.trimEnd() + "\n";
|
||||
await writeTextFile(getNoteFilePath(filename), body);
|
||||
}
|
||||
|
||||
export async function deleteNoteFile(filename: string): Promise<void> {
|
||||
await deleteFileIfExists(getNoteFilePath(filename));
|
||||
}
|
||||
|
||||
export async function fileExists(filename: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(getNoteFilePath(filename));
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
23
backend/src/notes/notes.types.ts
Normal file
23
backend/src/notes/notes.types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export type SyncStatus = "pending" | "syncing" | "synced" | "error";
|
||||
|
||||
export type NoteIndexItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
filename: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
sync: {
|
||||
status: SyncStatus;
|
||||
lastSyncedAt?: string;
|
||||
anythingllmLocation?: string;
|
||||
anythingllmDocumentId?: string;
|
||||
lastError?: string;
|
||||
cleanupPending?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type NoteContent = {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
};
|
||||
27
backend/src/queue/cleanup.worker.ts
Normal file
27
backend/src/queue/cleanup.worker.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { removeDocuments } from "../anythingllm/anythingllm.client.js";
|
||||
import type { CleanupJob } from "./queue.js";
|
||||
import { logError } from "../utils/logger.js";
|
||||
import { readIndex, writeIndex } from "../notes/notes.storage.js";
|
||||
|
||||
export async function handleCleanupJob(job: CleanupJob): Promise<void> {
|
||||
try {
|
||||
await removeDocuments([job.oldLocation]);
|
||||
const index = await readIndex();
|
||||
const updated = index.map((item) => {
|
||||
if (item.id === job.id) {
|
||||
return {
|
||||
...item,
|
||||
sync: {
|
||||
...item.sync,
|
||||
cleanupPending: false
|
||||
}
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
await writeIndex(updated);
|
||||
} catch (error) {
|
||||
logError("Temizlik kuyrugu hatasi", { error: (error as Error).message });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
86
backend/src/queue/queue.ts
Normal file
86
backend/src/queue/queue.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import path from "path";
|
||||
import { readJsonFile, writeJsonFile } from "../utils/fileUtils.js";
|
||||
import { config } from "../config.js";
|
||||
import { logError, logInfo } from "../utils/logger.js";
|
||||
|
||||
export type CleanupJob = {
|
||||
id: string;
|
||||
oldLocation: string;
|
||||
attempts: number;
|
||||
nextRunAt: string;
|
||||
};
|
||||
|
||||
const backoffSeconds = [5, 15, 30];
|
||||
|
||||
export class CleanupQueue {
|
||||
private jobs: CleanupJob[] = [];
|
||||
private running = false;
|
||||
private timer?: NodeJS.Timeout;
|
||||
private readonly filePath: string;
|
||||
private readonly handler: (job: CleanupJob) => Promise<void>;
|
||||
|
||||
constructor(handler: (job: CleanupJob) => Promise<void>) {
|
||||
this.handler = handler;
|
||||
this.filePath = path.join(config.NOTES_DIR, "cleanup-queue.json");
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
this.jobs = await readJsonFile<CleanupJob[]>(this.filePath, []);
|
||||
}
|
||||
|
||||
async persist(): Promise<void> {
|
||||
await writeJsonFile(this.filePath, this.jobs);
|
||||
}
|
||||
|
||||
async enqueue(job: CleanupJob): Promise<void> {
|
||||
this.jobs.push(job);
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.timer) {
|
||||
return;
|
||||
}
|
||||
this.timer = setInterval(() => void this.tick(), 1000);
|
||||
logInfo("Temizlik kuyrugu calisiyor");
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async tick(): Promise<void> {
|
||||
if (this.running) {
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
const jobIndex = this.jobs.findIndex((job) => new Date(job.nextRunAt).getTime() <= now);
|
||||
if (jobIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const job = this.jobs[jobIndex];
|
||||
this.running = true;
|
||||
try {
|
||||
await this.handler(job);
|
||||
this.jobs.splice(jobIndex, 1);
|
||||
await this.persist();
|
||||
logInfo("Temizlik basarili", { jobId: job.id });
|
||||
} catch (error) {
|
||||
job.attempts += 1;
|
||||
if (job.attempts >= backoffSeconds.length) {
|
||||
logError("Temizlik en fazla deneme sayisina ulasti", { jobId: job.id });
|
||||
this.jobs.splice(jobIndex, 1);
|
||||
} else {
|
||||
const delay = backoffSeconds[job.attempts - 1] * 1000;
|
||||
job.nextRunAt = new Date(Date.now() + delay).toISOString();
|
||||
}
|
||||
await this.persist();
|
||||
} finally {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
backend/src/server.ts
Normal file
45
backend/src/server.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import morgan from "morgan";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { config } from "./config.js";
|
||||
import { basicAuth } from "./auth/basicAuth.js";
|
||||
import { notesRoutes } from "./notes/notes.routes.js";
|
||||
import { initNotesStorage } from "./notes/notes.storage.js";
|
||||
import { CleanupQueue } from "./queue/queue.js";
|
||||
import { handleCleanupJob } from "./queue/cleanup.worker.js";
|
||||
import { logInfo } from "./utils/logger.js";
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: "2mb" }));
|
||||
app.use(morgan("dev"));
|
||||
|
||||
await initNotesStorage();
|
||||
|
||||
const cleanupQueue = new CleanupQueue(handleCleanupJob);
|
||||
await cleanupQueue.load();
|
||||
cleanupQueue.start();
|
||||
|
||||
const apiRouter = express.Router();
|
||||
apiRouter.get("/health", (_req, res) => {
|
||||
res.json({ status: "ok" });
|
||||
});
|
||||
apiRouter.use(notesRoutes(cleanupQueue));
|
||||
|
||||
app.use("/api", basicAuth, apiRouter);
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const publicDir = path.resolve(__dirname, "public");
|
||||
app.use(express.static(publicDir));
|
||||
app.get("*", (_req, res) => {
|
||||
res.sendFile(path.join(publicDir, "index.html"));
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(config.PORT, "0.0.0.0", () => {
|
||||
logInfo(`Sunucu basladi: ${config.PORT}`);
|
||||
});
|
||||
41
backend/src/utils/fileUtils.ts
Normal file
41
backend/src/utils/fileUtils.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
export async function ensureDir(dirPath: string): Promise<void> {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
export async function readJsonFile<T>(filePath: string, fallback: T): Promise<T> {
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, "utf-8");
|
||||
return JSON.parse(raw) as T;
|
||||
} catch (error) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeJsonFile<T>(filePath: string, data: T): Promise<void> {
|
||||
const dir = path.dirname(filePath);
|
||||
await ensureDir(dir);
|
||||
const tmpPath = `${filePath}.tmp`;
|
||||
await fs.writeFile(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
||||
await fs.rename(tmpPath, filePath);
|
||||
}
|
||||
|
||||
export async function readTextFile(filePath: string): Promise<string> {
|
||||
return fs.readFile(filePath, "utf-8");
|
||||
}
|
||||
|
||||
export async function writeTextFile(filePath: string, content: string): Promise<void> {
|
||||
const dir = path.dirname(filePath);
|
||||
await ensureDir(dir);
|
||||
await fs.writeFile(filePath, content, "utf-8");
|
||||
}
|
||||
|
||||
export async function deleteFileIfExists(filePath: string): Promise<void> {
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
15
backend/src/utils/logger.ts
Normal file
15
backend/src/utils/logger.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function logInfo(message: string, meta?: Record<string, unknown>): void {
|
||||
if (meta) {
|
||||
console.log(`[info] ${message}`, meta);
|
||||
return;
|
||||
}
|
||||
console.log(`[info] ${message}`);
|
||||
}
|
||||
|
||||
export function logError(message: string, meta?: Record<string, unknown>): void {
|
||||
if (meta) {
|
||||
console.error(`[error] ${message}`, meta);
|
||||
return;
|
||||
}
|
||||
console.error(`[error] ${message}`);
|
||||
}
|
||||
6
backend/src/utils/slugify.ts
Normal file
6
backend/src/utils/slugify.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function slugify(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)+/g, "");
|
||||
}
|
||||
Reference in New Issue
Block a user