first commit

This commit is contained in:
ysj7705 committed 2026-07-15 17:29:25 +09:00
commit a74fe7a388
42 files changed
+6058

No files matched your search

+105
View File
@@ -0,0 +1,105 @@
import { config } from '../config.js';
import type { SessionDoc } from '../types/session.js';
import { signOnlyOfficePayload } from '../utils/jwt.js';
export function buildEditorConfig(doc: SessionDoc, userId: string) {
const contentUrl = `${config.internalUrl}/api/documents/${doc.docId}/content?token=${doc.token}`;
const callbackUrl = `${config.internalUrl}/api/documents/${doc.docId}/callback`;
const document = {
fileType: doc.fileType,
key: doc.key,
title: doc.filename,
url: contentUrl,
permissions: {
edit: true,
download: true,
print: true,
},
};
const editorConfig = {
callbackUrl,
mode: 'edit' as const,
lang: 'ko',
user: {
id: userId,
name: 'Guest',
},
customization: {
forcesave: true,
goback: false,
},
};
const payload = {
documentType: doc.documentType,
document,
editorConfig,
};
const token = signOnlyOfficePayload(payload);
return {
documentType: doc.documentType,
document,
editorConfig,
token,
documentServerUrl: config.onlyOfficeUrl,
};
}
export async function convertToOriginalFormat(
sourceUrl: string,
fromExt: string,
toExt: string,
): Promise<Buffer | null> {
if (fromExt === toExt) return null;
const convertUrl = `${config.onlyOfficeInternalUrl}/ConvertService.ashx`;
const key = `conv_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
const body = {
async: false,
filetype: fromExt,
key,
outputtype: toExt,
url: sourceUrl,
};
const token = signOnlyOfficePayload(body);
try {
const res = await fetch(convertUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ ...body, token }),
});
if (!res.ok) {
console.error('[convert] HTTP', res.status);
return null;
}
const data = (await res.json()) as {
error?: number;
fileUrl?: string;
endConvert?: boolean;
};
if (data.error || !data.fileUrl) {
console.error('[convert] failed', data);
return null;
}
const fileRes = await fetch(data.fileUrl);
if (!fileRes.ok) return null;
return Buffer.from(await fileRes.arrayBuffer());
} catch (err) {
console.error('[convert] error', err);
return null;
}
}
@@ -0,0 +1,160 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { randomBytes } from 'node:crypto';
import { v4 as uuidv4 } from 'uuid';
import { config } from '../config.js';
import type { Session, SessionDoc } from '../types/session.js';
import { documentTypeFor, mimeFor } from '../utils/formats.js';
const sessions = new Map<string, Session>();
function now(): number {
return Date.now();
}
function makeKey(docId: string, version = 0): string {
return `${docId}_${version}_${randomBytes(4).toString('hex')}`;
}
export async function ensureTempRoot(): Promise<void> {
await fs.mkdir(config.tempDir, { recursive: true });
}
export async function wipeTempRoot(): Promise<void> {
try {
await fs.rm(config.tempDir, { recursive: true, force: true });
} catch {
/* ignore */
}
await ensureTempRoot();
sessions.clear();
}
export function createSession(): Session {
const sessionId = uuidv4();
const t = now();
const session: Session = {
sessionId,
lastSeenAt: t,
createdAt: t,
docs: new Map(),
};
sessions.set(sessionId, session);
return session;
}
export function getSession(sessionId: string): Session | undefined {
return sessions.get(sessionId);
}
export function touchSession(sessionId: string): boolean {
const session = sessions.get(sessionId);
if (!session) return false;
session.lastSeenAt = now();
return true;
}
export async function deleteSession(sessionId: string): Promise<boolean> {
const session = sessions.get(sessionId);
if (!session) return false;
const dir = path.join(config.tempDir, sessionId);
sessions.delete(sessionId);
await fs.rm(dir, { recursive: true, force: true });
return true;
}
export async function addDocument(
sessionId: string,
originalFilename: string,
fileType: string,
buffer: Buffer,
): Promise<SessionDoc> {
const session = sessions.get(sessionId);
if (!session) {
throw new Error('SESSION_NOT_FOUND');
}
const docId = uuidv4();
const token = randomBytes(24).toString('hex');
const dir = path.join(config.tempDir, sessionId, docId);
await fs.mkdir(dir, { recursive: true });
const filePath = path.join(dir, `document.${fileType}`);
await fs.writeFile(filePath, buffer);
const t = now();
const doc: SessionDoc = {
sessionId,
docId,
key: makeKey(docId),
filename: originalFilename,
originalFilename,
mime: mimeFor(fileType),
fileType,
documentType: documentTypeFor(fileType),
path: filePath,
token,
lastSeenAt: t,
createdAt: t,
};
session.docs.set(docId, doc);
session.lastSeenAt = t;
return doc;
}
export function getDocument(docId: string): SessionDoc | undefined {
for (const session of sessions.values()) {
const doc = session.docs.get(docId);
if (doc) return doc;
}
return undefined;
}
export async function overwriteDocument(
doc: SessionDoc,
buffer: Buffer,
fileType?: string,
): Promise<void> {
const ext = fileType ?? doc.fileType;
if (ext !== doc.fileType) {
throw new Error('FILETYPE_MISMATCH');
}
await fs.writeFile(doc.path, buffer);
doc.lastSeenAt = now();
doc.key = makeKey(doc.docId, now());
const session = sessions.get(doc.sessionId);
if (session) session.lastSeenAt = doc.lastSeenAt;
}
export async function deleteDocument(docId: string): Promise<boolean> {
for (const session of sessions.values()) {
const doc = session.docs.get(docId);
if (!doc) continue;
session.docs.delete(docId);
await fs.rm(path.dirname(doc.path), { recursive: true, force: true });
return true;
}
return false;
}
export async function sweepExpired(): Promise<number> {
const cutoff = now() - config.sessionTtlMs;
let removed = 0;
for (const [id, session] of sessions) {
if (session.lastSeenAt < cutoff) {
await deleteSession(id);
removed += 1;
}
}
return removed;
}
export function startSweeper(intervalMs = 60_000): NodeJS.Timeout {
return setInterval(() => {
void sweepExpired().then((n) => {
if (n > 0) {
console.log(`[session] swept ${n} expired session(s)`);
}
});
}, intervalMs);
}