From eb91d95ec06f45cb072201a0aeace8f98a464e79 Mon Sep 17 00:00:00 2001 From: AlexIves16 Date: Fri, 24 Apr 2026 01:18:27 +0500 Subject: [PATCH] feat: support personal user repos for canvas storage --- app/server/utils/giteaApi.ts | 56 ++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/app/server/utils/giteaApi.ts b/app/server/utils/giteaApi.ts index be479fc..83bb664 100644 --- a/app/server/utils/giteaApi.ts +++ b/app/server/utils/giteaApi.ts @@ -1,6 +1,6 @@ // Gitea API integration for XFlow canvas sync // Structure: -// Personal canvases → AVTEX/user-{username} (created on first save) +// Personal canvases → {username}/MM (personal user repo, created on first save) // Group canvases → AVTEX/group-{groupId} (created on group creation) // Public canvases → AVTEX/MMAVTEX (shared org repo) @@ -27,50 +27,56 @@ const apiHeaders = (token: string) => ({ }) // Resolve which repo to use based on canvas ownership -export function resolveRepo(opts: { username?: string; groupId?: string; isPublic?: boolean }): string { +// Returns { owner, repo } to support both org and personal repos +export function resolveRepo(opts: { username?: string; groupId?: string; isPublic?: boolean }): { owner: string; repo: string } { const { org, mindmapRepo } = getConfig() - if (opts.groupId) return `group-${opts.groupId}` - if (opts.username && !opts.isPublic) return `user-${opts.username}` - return mindmapRepo + if (opts.groupId) return { owner: org, repo: `group-${opts.groupId}` } + if (opts.username && !opts.isPublic) return { owner: opts.username, repo: 'MM' } + return { owner: org, repo: mindmapRepo } } -// Ensure repository exists in org, create if not -export async function ensureRepo(repoName: string, description = ''): Promise { +// Ensure repository exists, create if not (supports personal and org repos) +export async function ensureRepo(owner: string, 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 }) + await $fetch(`${url}/api/v1/repos/${owner}/${repoName}`, { headers: h }) return true } catch { } - // Create + // Create — personal repo or org repo try { - await $fetch(`${url}/api/v1/orgs/${org}/repos`, { + const isOrg = owner === org + const endpoint = isOrg + ? `${url}/api/v1/orgs/${org}/repos` + : `${url}/api/v1/user/repos` + await $fetch(endpoint, { method: 'POST', headers: h, body: { name: repoName, description: description || `XFlow canvas repo: ${repoName}`, - private: !repoName.startsWith('group-') && repoName !== getConfig().mindmapRepo, + private: true, auto_init: true, default_branch: 'main' } }) return true } catch (e) { - console.error(`Gitea ensureRepo(${repoName}) error:`, e) + console.error(`Gitea ensureRepo(${owner}/${repoName}) error:`, e) return false } } // Get file from Gitea (returns null if not found) -export async function giteaGetFile(path: string, repo?: string): Promise { +export async function giteaGetFile(path: string, owner?: string, repo?: string): Promise { const { url, token, org, mindmapRepo } = getConfig() + const repoOwner = owner || org const repoName = repo || mindmapRepo try { - const res = await $fetch(`${url}/api/v1/repos/${org}/${repoName}/contents/${path}`, { + const res = await $fetch(`${url}/api/v1/repos/${repoOwner}/${repoName}/contents/${path}`, { headers: apiHeaders(token) }) return { content: res.content, sha: res.sha } @@ -80,13 +86,14 @@ export async function giteaGetFile(path: string, repo?: string): Promise { +export async function giteaPutFile(path: string, content: string, message: string, sha?: string, owner?: string, repo?: string): Promise { const { url, token, org, mindmapRepo } = getConfig() + const repoOwner = owner || org const repoName = repo || mindmapRepo const encoded = Buffer.from(content).toString('base64') try { - await $fetch(`${url}/api/v1/repos/${org}/${repoName}/contents/${path}`, { + await $fetch(`${url}/api/v1/repos/${repoOwner}/${repoName}/contents/${path}`, { method: sha ? 'PUT' : 'POST', headers: apiHeaders(token), body: { @@ -109,14 +116,14 @@ export async function saveCanvasToGitea( opts: { username?: string; groupId?: string; isPublic?: boolean } = {}, commitMessage?: string ): Promise { - const repo = resolveRepo(opts) + const { owner, repo } = resolveRepo(opts) const path = `${canvasId}.canvas` const content = JSON.stringify(canvasData, null, 2) const message = commitMessage || `Update ${canvasId} at ${new Date().toLocaleString('ru-RU')}` - await ensureRepo(repo) - const existing = await giteaGetFile(path, repo) - return giteaPutFile(path, content, message, existing?.sha, repo) + await ensureRepo(owner, repo) + const existing = await giteaGetFile(path, owner, repo) + return giteaPutFile(path, content, message, existing?.sha, owner, repo) } // Load canvas from Gitea @@ -124,8 +131,8 @@ 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) + const { owner, repo } = resolveRepo(opts) + const file = await giteaGetFile(`${canvasId}.canvas`, owner, repo) if (!file) return null try { @@ -136,11 +143,12 @@ export async function loadCanvasFromGitea( } // List canvases in a repo -export async function listCanvasesFromGitea(repo?: string): Promise { +export async function listCanvasesFromGitea(owner?: string, repo?: string): Promise { const { url, token, org, mindmapRepo } = getConfig() + const repoOwner = owner || org const repoName = repo || mindmapRepo try { - const res = await $fetch(`${url}/api/v1/repos/${org}/${repoName}/contents/`, { + const res = await $fetch(`${url}/api/v1/repos/${repoOwner}/${repoName}/contents/`, { headers: apiHeaders(token) }) return res