feat: add git API layer with Gitea and GitHub implementations
GitHost interface for reading/writing vault files via REST API. Gitea and GitHub implementations handle base64 content encoding, SHA-based updates, and directory listing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
114
extension/src/service-worker/gitea.ts
Normal file
114
extension/src/service-worker/gitea.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import type { GitHost } from './git-host';
|
||||
import { uint8ArrayToBase64, base64ToUint8Array } from './git-host';
|
||||
|
||||
/// Gitea Contents API implementation.
|
||||
///
|
||||
/// Endpoints:
|
||||
/// GET {hostUrl}/api/v1/repos/{repoPath}/contents/{path}
|
||||
/// POST {hostUrl}/api/v1/repos/{repoPath}/contents/{path} (create)
|
||||
/// PUT {hostUrl}/api/v1/repos/{repoPath}/contents/{path} (update)
|
||||
/// DELETE {hostUrl}/api/v1/repos/{repoPath}/contents/{path}
|
||||
///
|
||||
/// Auth: `token {apiToken}` header.
|
||||
/// Content is base64-encoded in both request and response bodies.
|
||||
/// Updates and deletes require the current file SHA.
|
||||
|
||||
export class GiteaHost implements GitHost {
|
||||
private baseUrl: string;
|
||||
private headers: Record<string, string>;
|
||||
|
||||
constructor(hostUrl: string, repoPath: string, apiToken: string) {
|
||||
// Remove trailing slash from hostUrl
|
||||
const host = hostUrl.replace(/\/+$/, '');
|
||||
this.baseUrl = `${host}/api/v1/repos/${repoPath}/contents`;
|
||||
this.headers = {
|
||||
'Authorization': `token ${apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
}
|
||||
|
||||
async readFile(path: string): Promise<Uint8Array> {
|
||||
const resp = await fetch(`${this.baseUrl}/${path}`, {
|
||||
headers: this.headers,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Gitea readFile ${path}: ${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
const json = await resp.json();
|
||||
// Gitea returns base64 content with possible newlines
|
||||
const clean = (json.content as string).replace(/\n/g, '');
|
||||
return base64ToUint8Array(clean);
|
||||
}
|
||||
|
||||
async writeFile(path: string, content: Uint8Array, message: string): Promise<void> {
|
||||
const b64 = uint8ArrayToBase64(content);
|
||||
|
||||
// Try to get the current SHA for an update; if 404 it's a create.
|
||||
let sha: string | null = null;
|
||||
try {
|
||||
const existing = await fetch(`${this.baseUrl}/${path}`, {
|
||||
headers: this.headers,
|
||||
});
|
||||
if (existing.ok) {
|
||||
const json = await existing.json();
|
||||
sha = json.sha as string;
|
||||
}
|
||||
} catch {
|
||||
// File does not exist — will create.
|
||||
}
|
||||
|
||||
const body: Record<string, string> = { content: b64, message };
|
||||
if (sha) {
|
||||
body.sha = sha;
|
||||
}
|
||||
|
||||
const method = sha ? 'PUT' : 'POST';
|
||||
const resp = await fetch(`${this.baseUrl}/${path}`, {
|
||||
method,
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text();
|
||||
throw new Error(`Gitea writeFile ${path}: ${resp.status} ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(path: string, message: string): Promise<void> {
|
||||
// Need the current SHA to delete.
|
||||
const existing = await fetch(`${this.baseUrl}/${path}`, {
|
||||
headers: this.headers,
|
||||
});
|
||||
if (!existing.ok) {
|
||||
throw new Error(`Gitea deleteFile ${path}: file not found (${existing.status})`);
|
||||
}
|
||||
const json = await existing.json();
|
||||
const sha = json.sha as string;
|
||||
|
||||
const resp = await fetch(`${this.baseUrl}/${path}`, {
|
||||
method: 'DELETE',
|
||||
headers: this.headers,
|
||||
body: JSON.stringify({ message, sha }),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text();
|
||||
throw new Error(`Gitea deleteFile ${path}: ${resp.status} ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
async listDir(path: string): Promise<string[]> {
|
||||
const resp = await fetch(`${this.baseUrl}/${path}`, {
|
||||
headers: this.headers,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
if (resp.status === 404) return [];
|
||||
throw new Error(`Gitea listDir ${path}: ${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
const json = await resp.json();
|
||||
if (!Array.isArray(json)) return [];
|
||||
return json.map((item: { name: string }) => item.name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user