2e1f4d7929
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
2.8 KiB
TypeScript
79 lines
2.8 KiB
TypeScript
import type { TransactionType } from "./transactions";
|
||
|
||
/** Ek dosya kuralları (Bölüm 6.3, 7.4). Sunucu ve istemci aynı sabitleri kullanır. */
|
||
|
||
export const MAX_ATTACHMENTS_PER_RECORD = 10;
|
||
export const ATTACHMENT_KINDS = ["image", "pdf"] as const;
|
||
export type AttachmentKind = (typeof ATTACHMENT_KINDS)[number];
|
||
|
||
/** Dosya içeriğinden tespit edilen kaynak türler (uzantıya güvenilmez). */
|
||
export type DetectedType = "jpeg" | "png" | "webp" | "heic" | "pdf";
|
||
|
||
export function kindOf(type: DetectedType): AttachmentKind {
|
||
return type === "pdf" ? "pdf" : "image";
|
||
}
|
||
|
||
/**
|
||
* İşlem türüne göre izinli ek türleri: gider yalnızca görsel (fiş/fatura fotoğrafı);
|
||
* gelir görsel veya PDF (ör. bordro); transfer görsel veya PDF (dekont).
|
||
*/
|
||
export function allowedAttachmentKinds(type: TransactionType): readonly AttachmentKind[] {
|
||
return type === "expense" ? ["image"] : ["image", "pdf"];
|
||
}
|
||
|
||
/** `<input accept>` değeri (yalnızca seçici filtresi; asıl doğrulama sunucuda). */
|
||
export function acceptAttribute(kinds: readonly AttachmentKind[]): string {
|
||
const image = "image/jpeg,image/png,image/webp,image/heic,image/heif,.heic,.heif";
|
||
return kinds.includes("pdf") ? `${image},application/pdf,.pdf` : image;
|
||
}
|
||
|
||
/** Dosya içeriğinin ilk baytlarından türü bulur; desteklenmeyen içerikte null. */
|
||
export function detectFileType(bytes: Uint8Array): DetectedType | null {
|
||
const at = (offset: number, sig: number[]) =>
|
||
bytes.length >= offset + sig.length && sig.every((b, i) => bytes[offset + i] === b);
|
||
const ascii = (offset: number, text: string) =>
|
||
at(
|
||
offset,
|
||
[...text].map((c) => c.charCodeAt(0)),
|
||
);
|
||
|
||
if (at(0, [0xff, 0xd8, 0xff])) return "jpeg";
|
||
if (at(0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return "png";
|
||
if (ascii(0, "RIFF") && ascii(8, "WEBP")) return "webp";
|
||
if (ascii(0, "%PDF-")) return "pdf";
|
||
// ISO BMFF: [boyut][ftyp][ana marka][sürüm][uyumlu markalar…]
|
||
if (ascii(4, "ftyp")) {
|
||
const boxSize = Math.min(
|
||
bytes.length,
|
||
((bytes[0]! << 24) | (bytes[1]! << 16) | (bytes[2]! << 8) | bytes[3]!) >>> 0,
|
||
);
|
||
const brands: string[] = [];
|
||
const brandAt = (o: number) => String.fromCharCode(...bytes.subarray(o, o + 4));
|
||
brands.push(brandAt(8));
|
||
for (let o = 16; o + 4 <= boxSize && o < 64; o += 4) brands.push(brandAt(o));
|
||
if (brands.some((b) => HEIF_BRANDS.has(b))) return "heic";
|
||
}
|
||
return null;
|
||
}
|
||
|
||
const HEIF_BRANDS = new Set([
|
||
"heic",
|
||
"heix",
|
||
"heim",
|
||
"heis",
|
||
"hevc",
|
||
"hevx",
|
||
"hevm",
|
||
"hevs",
|
||
"mif1",
|
||
"msf1",
|
||
]);
|
||
|
||
/** "1,2 MB" gibi okunur boyut. */
|
||
export function formatFileSize(bytes: number): string {
|
||
if (bytes < 1024) return `${bytes} B`;
|
||
const kb = bytes / 1024;
|
||
if (kb < 1024) return `${Math.round(kb)} KB`;
|
||
return `${(kb / 1024).toLocaleString("tr-TR", { maximumFractionDigits: 1 })} MB`;
|
||
}
|