diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index c3e8c18..f760b0d 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -3,7 +3,7 @@ name: Deploy XFlow (AVTEX/Projects) on: push: branches: - - main + - master workflow_dispatch: jobs: diff --git a/app/server/utils/giteaApi.ts b/app/server/utils/giteaApi.ts index e2e69fa..be479fc 100644 --- a/app/server/utils/giteaApi.ts +++ b/app/server/utils/giteaApi.ts @@ -1,5 +1,8 @@ // Gitea API integration for XFlow canvas sync -// Uses Gitea REST API to store mind maps in MMAVTEX repo +// Structure: +// Personal canvases → AVTEX/user-{username} (created on first save) +// Group canvases → AVTEX/group-{groupId} (created on group creation) +// Public canvases → AVTEX/MMAVTEX (shared org repo) interface GiteaFile { content: string // base64 @@ -17,19 +20,58 @@ const getConfig = () => { } } -const headers = (token: string) => ({ +const apiHeaders = (token: string) => ({ 'Authorization': `token ${token}`, 'Content-Type': 'application/json', 'Accept': 'application/json' }) +// Resolve which repo to use based on canvas ownership +export function resolveRepo(opts: { username?: string; groupId?: string; isPublic?: boolean }): string { + const { org, mindmapRepo } = getConfig() + if (opts.groupId) return `group-${opts.groupId}` + if (opts.username && !opts.isPublic) return `user-${opts.username}` + return mindmapRepo +} + +// Ensure repository exists in org, create if not +export async function ensureRepo(repoName: string, description = ''): Promise { + const { url, token, org } = getConfig() + const h = apiHeaders(token) + + // Check if exists + try { + await $fetch(`${url}/api/v1/repos/${org}/${repoName}`, { headers: h }) + return true + } catch { } + + // Create + try { + await $fetch(`${url}/api/v1/orgs/${org}/repos`, { + method: 'POST', + headers: h, + body: { + name: repoName, + description: description || `XFlow canvas repo: ${repoName}`, + private: !repoName.startsWith('group-') && repoName !== getConfig().mindmapRepo, + auto_init: true, + default_branch: 'main' + } + }) + return true + } catch (e) { + console.error(`Gitea ensureRepo(${repoName}) error:`, e) + return false + } +} + // Get file from Gitea (returns null if not found) export async function giteaGetFile(path: string, repo?: string): Promise { const { url, token, org, mindmapRepo } = getConfig() const repoName = repo || mindmapRepo try { const res = await $fetch(`${url}/api/v1/repos/${org}/${repoName}/contents/${path}`, { - headers: headers(token) + headers: apiHeaders(token) }) return { content: res.content, sha: res.sha } } catch { @@ -46,7 +88,7 @@ export async function giteaPutFile(path: string, content: string, message: strin try { await $fetch(`${url}/api/v1/repos/${org}/${repoName}/contents/${path}`, { method: sha ? 'PUT' : 'POST', - headers: headers(token), + headers: apiHeaders(token), body: { message, content: encoded, @@ -60,37 +102,46 @@ export async function giteaPutFile(path: string, content: string, message: strin } } -// Save canvas to Gitea MMAVTEX repo -export async function saveCanvasToGitea(canvasId: string, canvasData: any, commitMessage?: string): Promise { - const path = `canvases/${canvasId}.canvas` +// Save canvas to correct repo based on ownership +export async function saveCanvasToGitea( + canvasId: string, + canvasData: any, + opts: { username?: string; groupId?: string; isPublic?: boolean } = {}, + commitMessage?: string +): Promise { + const repo = resolveRepo(opts) + const path = `${canvasId}.canvas` const content = JSON.stringify(canvasData, null, 2) - const message = commitMessage || `Update canvas ${canvasId} at ${new Date().toLocaleString('ru-RU')}` + const message = commitMessage || `Update ${canvasId} at ${new Date().toLocaleString('ru-RU')}` - // Get existing file SHA if exists (required for update) - const existing = await giteaGetFile(path) - return giteaPutFile(path, content, message, existing?.sha) + await ensureRepo(repo) + const existing = await giteaGetFile(path, repo) + return giteaPutFile(path, content, message, existing?.sha, repo) } // Load canvas from Gitea -export async function loadCanvasFromGitea(canvasId: string): Promise { - const path = `canvases/${canvasId}.canvas` - const file = await giteaGetFile(path) +export async function loadCanvasFromGitea( + canvasId: string, + opts: { username?: string; groupId?: string; isPublic?: boolean } = {} +): Promise { + const repo = resolveRepo(opts) + const file = await giteaGetFile(`${canvasId}.canvas`, repo) if (!file) return null try { - const decoded = Buffer.from(file.content, 'base64').toString('utf-8') - return JSON.parse(decoded) + return JSON.parse(Buffer.from(file.content, 'base64').toString('utf-8')) } catch { return null } } -// List all canvases in Gitea -export async function listCanvasesFromGitea(): Promise { +// List canvases in a repo +export async function listCanvasesFromGitea(repo?: string): Promise { const { url, token, org, mindmapRepo } = getConfig() + const repoName = repo || mindmapRepo try { - const res = await $fetch(`${url}/api/v1/repos/${org}/${mindmapRepo}/contents/canvases`, { - headers: headers(token) + const res = await $fetch(`${url}/api/v1/repos/${org}/${repoName}/contents/`, { + headers: apiHeaders(token) }) return res .filter((f: any) => f.name.endsWith('.canvas'))