github-actions[bot]
commited on
Commit
·
01ab30b
1
Parent(s):
1e6d48a
Update from GitHub Actions
Browse files- functions/api/hf/[[path]].ts +102 -56
- package-lock.json +19 -0
- package.json +1 -0
- src/services/repoApi.ts +104 -41
- src/views/ContentView.vue +3 -4
- src/views/UploadView.vue +4 -9
functions/api/hf/[[path]].ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
-
//没有固定的文档
|
| 3 |
-
//接口是通过查看huggingface.co的请求来实现的
|
| 4 |
-
//有个huggingface hub api
|
| 5 |
-
//https://huggingface.co/docs/huggingface.js/hub/README
|
| 6 |
-
//https://github.com/huggingface/huggingface.js/tree/main/packages/hub
|
| 7 |
-
//https://github.com/huggingface/huggingface.js/blob/main/packages/hub/src/lib/list-files.ts
|
| 8 |
-
//https://github.com/huggingface/huggingface.js/blob/main/packages/hub/src/lib/download-file.ts
|
| 9 |
-
//https://github.com/huggingface/huggingface.js/blob/main/packages/hub/src/lib/commit.ts
|
| 10 |
-
//https://github.com/huggingface/huggingface.js/blob/main/packages/hub/src/lib/delete-files.ts
|
| 11 |
export const onRequest = async (context: RouteContext): Promise<Response> => {
|
| 12 |
const request = context.request;
|
| 13 |
const env = context.env as Env;
|
|
@@ -39,71 +38,118 @@ export const onRequest = async (context: RouteContext): Promise<Response> => {
|
|
| 39 |
headers: { 'Content-Type': 'application/json' }
|
| 40 |
});
|
| 41 |
}
|
| 42 |
-
// Hugging Face API 基础 URL
|
| 43 |
-
const hfApiBaseUrl = 'https://huggingface.co/api/datasets';
|
| 44 |
-
|
| 45 |
-
// 处理 GET 请求 - 获取文件内容或列出文件
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
|
|
|
| 48 |
if (operation === 'raw' && request.method === 'GET') {
|
| 49 |
const path = pathParts.length > 6 ? pathParts.slice(6).join('/') : '';
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
| 64 |
}
|
| 65 |
|
| 66 |
if (operation === 'tree' && request.method === 'GET') {
|
| 67 |
const path = pathParts.length > 6 ? pathParts.slice(6).join('/') : '';
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
-
});
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
// 处理 POST 请求 - 上传文件
|
| 86 |
if (operation === 'commit' && (request.method === 'POST' || request.method === 'PUT' || request.method === 'DELETE')) {
|
| 87 |
-
const
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
}
|
| 105 |
|
| 106 |
-
|
| 107 |
// 不支持的请求方法或路径
|
| 108 |
return new Response(JSON.stringify({ error: '不支持的请求方法或路径' }), {
|
| 109 |
status: 400,
|
|
|
|
| 1 |
+
import { downloadFile, commit, listFiles, RepoType, CommitOperation } from "@huggingface/hub";
|
| 2 |
+
|
| 3 |
+
interface CommitBody {
|
| 4 |
+
path: string;
|
| 5 |
+
content?: string;
|
| 6 |
+
message?: string;
|
| 7 |
+
lfs?: boolean;
|
| 8 |
+
}
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
export const onRequest = async (context: RouteContext): Promise<Response> => {
|
| 11 |
const request = context.request;
|
| 12 |
const env = context.env as Env;
|
|
|
|
| 38 |
headers: { 'Content-Type': 'application/json' }
|
| 39 |
});
|
| 40 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
const apiParams = {
|
| 43 |
+
repo: {
|
| 44 |
+
name: `${owner}/${repo}`,
|
| 45 |
+
type: 'dataset' as RepoType
|
| 46 |
+
},
|
| 47 |
+
accessToken: hfToken,
|
| 48 |
+
revision: ref
|
| 49 |
+
};
|
| 50 |
|
| 51 |
+
// 处理 GET 请求 - 获取文件内容或列出文件
|
| 52 |
if (operation === 'raw' && request.method === 'GET') {
|
| 53 |
const path = pathParts.length > 6 ? pathParts.slice(6).join('/') : '';
|
| 54 |
+
|
| 55 |
+
try {
|
| 56 |
+
const response = await downloadFile({
|
| 57 |
+
...apiParams,
|
| 58 |
+
path: path
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
if (!response) {
|
| 62 |
+
return new Response(JSON.stringify({ error: '文件不存在' }), {
|
| 63 |
+
status: 404,
|
| 64 |
+
headers: { 'Content-Type': 'application/json' }
|
| 65 |
+
});
|
| 66 |
}
|
| 67 |
+
return response;
|
| 68 |
+
} catch (error: any) {
|
| 69 |
+
return new Response(JSON.stringify({ error: '获取文件失败', details: error.message }), {
|
| 70 |
+
status: 500,
|
| 71 |
+
headers: { 'Content-Type': 'application/json' }
|
| 72 |
+
});
|
| 73 |
+
}
|
| 74 |
}
|
| 75 |
|
| 76 |
if (operation === 'tree' && request.method === 'GET') {
|
| 77 |
const path = pathParts.length > 6 ? pathParts.slice(6).join('/') : '';
|
| 78 |
+
try {
|
| 79 |
+
const cursor = listFiles({
|
| 80 |
+
...apiParams,
|
| 81 |
+
path: path
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
const files = [];
|
| 85 |
+
for await (const entry of cursor) {
|
| 86 |
+
files.push(entry);
|
| 87 |
}
|
|
|
|
| 88 |
|
| 89 |
+
return new Response(JSON.stringify(files), {
|
| 90 |
+
headers: { 'Content-Type': 'application/json' }
|
| 91 |
+
});
|
| 92 |
+
} catch (error: any) {
|
| 93 |
+
return new Response(JSON.stringify({ error: '列出文件失败', details: error.message }), {
|
| 94 |
+
status: 500,
|
| 95 |
+
headers: { 'Content-Type': 'application/json' }
|
| 96 |
+
});
|
| 97 |
+
}
|
| 98 |
}
|
| 99 |
|
| 100 |
// 处理 POST 请求 - 上传文件
|
| 101 |
if (operation === 'commit' && (request.method === 'POST' || request.method === 'PUT' || request.method === 'DELETE')) {
|
| 102 |
+
const body = await request.json() as CommitBody;
|
| 103 |
+
|
| 104 |
+
try {
|
| 105 |
+
let operation: CommitOperation;
|
| 106 |
+
|
| 107 |
+
if (request.method === 'DELETE') {
|
| 108 |
+
operation = {
|
| 109 |
+
operation: 'delete',
|
| 110 |
+
path: body.path
|
| 111 |
+
};
|
| 112 |
+
} else {
|
| 113 |
+
const content = body.content || '';
|
| 114 |
+
let blobContent: Blob;
|
| 115 |
+
|
| 116 |
+
if (body.lfs === true && body.content) {
|
| 117 |
+
const binary = atob(body.content);
|
| 118 |
+
const bytes = new Uint8Array(binary.length);
|
| 119 |
+
for (let i = 0; i < binary.length; i++) {
|
| 120 |
+
bytes[i] = binary.charCodeAt(i);
|
| 121 |
+
}
|
| 122 |
+
blobContent = new Blob([bytes]);
|
| 123 |
+
} else {
|
| 124 |
+
blobContent = new Blob([content]);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
operation = {
|
| 128 |
+
operation: 'addOrUpdate',
|
| 129 |
+
path: body.path,
|
| 130 |
+
content: blobContent
|
| 131 |
+
};
|
| 132 |
+
}
|
| 133 |
|
| 134 |
+
const response = await commit({
|
| 135 |
+
...apiParams,
|
| 136 |
+
operations: [operation],
|
| 137 |
+
title: `${request.method === 'DELETE' ? 'Delete' : 'Update'} ${body.path}`,
|
| 138 |
+
description: body.message || `Changed via API`
|
| 139 |
+
});
|
| 140 |
+
|
| 141 |
+
return new Response(JSON.stringify(response), {
|
| 142 |
+
status: 200,
|
| 143 |
+
headers: { 'Content-Type': 'application/json' }
|
| 144 |
+
});
|
| 145 |
+
} catch (error: any) {
|
| 146 |
+
return new Response(JSON.stringify({ error: '提交更改失败', details: error.message }), {
|
| 147 |
+
status: 500,
|
| 148 |
+
headers: { 'Content-Type': 'application/json' }
|
| 149 |
+
});
|
| 150 |
+
}
|
| 151 |
}
|
| 152 |
|
|
|
|
| 153 |
// 不支持的请求方法或路径
|
| 154 |
return new Response(JSON.stringify({ error: '不支持的请求方法或路径' }), {
|
| 155 |
status: 400,
|
package-lock.json
CHANGED
|
@@ -9,6 +9,7 @@
|
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@hono/node-server": "^1.13.8",
|
|
|
|
| 12 |
"@monaco-editor/loader": "^1.5.0",
|
| 13 |
"@tailwindcss/vite": "^4.0.14",
|
| 14 |
"dotenv": "^16.4.7",
|
|
@@ -796,6 +797,24 @@
|
|
| 796 |
"hono": "^4"
|
| 797 |
}
|
| 798 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 799 |
"node_modules/@img/sharp-darwin-arm64": {
|
| 800 |
"version": "0.33.5",
|
| 801 |
"resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
|
|
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@hono/node-server": "^1.13.8",
|
| 12 |
+
"@huggingface/hub": "^1.1.2",
|
| 13 |
"@monaco-editor/loader": "^1.5.0",
|
| 14 |
"@tailwindcss/vite": "^4.0.14",
|
| 15 |
"dotenv": "^16.4.7",
|
|
|
|
| 797 |
"hono": "^4"
|
| 798 |
}
|
| 799 |
},
|
| 800 |
+
"node_modules/@huggingface/hub": {
|
| 801 |
+
"version": "1.1.2",
|
| 802 |
+
"resolved": "https://registry.npmmirror.com/@huggingface/hub/-/hub-1.1.2.tgz",
|
| 803 |
+
"integrity": "sha512-Jf4GhvVj9ABDw4Itb3BV1T7f22iewuZva476qTicQ4kOTbosuUuFDhsVH7ZH6rVNgg20Ll9kaNBF5CXjySIT+w==",
|
| 804 |
+
"license": "MIT",
|
| 805 |
+
"dependencies": {
|
| 806 |
+
"@huggingface/tasks": "^0.18.2"
|
| 807 |
+
},
|
| 808 |
+
"engines": {
|
| 809 |
+
"node": ">=18"
|
| 810 |
+
}
|
| 811 |
+
},
|
| 812 |
+
"node_modules/@huggingface/tasks": {
|
| 813 |
+
"version": "0.18.3",
|
| 814 |
+
"resolved": "https://registry.npmmirror.com/@huggingface/tasks/-/tasks-0.18.3.tgz",
|
| 815 |
+
"integrity": "sha512-WCIg6tOSftCkE2WxSaDIZRsrs6bkHiH2qc4t6r8L/xoebAhQmIPEDU700zXI7ac1+I6UcCponYxn/oqu3TGNxQ==",
|
| 816 |
+
"license": "MIT"
|
| 817 |
+
},
|
| 818 |
"node_modules/@img/sharp-darwin-arm64": {
|
| 819 |
"version": "0.33.5",
|
| 820 |
"resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
package.json
CHANGED
|
@@ -16,6 +16,7 @@
|
|
| 16 |
},
|
| 17 |
"dependencies": {
|
| 18 |
"@hono/node-server": "^1.13.8",
|
|
|
|
| 19 |
"@monaco-editor/loader": "^1.5.0",
|
| 20 |
"@tailwindcss/vite": "^4.0.14",
|
| 21 |
"dotenv": "^16.4.7",
|
|
|
|
| 16 |
},
|
| 17 |
"dependencies": {
|
| 18 |
"@hono/node-server": "^1.13.8",
|
| 19 |
+
"@huggingface/hub": "^1.1.2",
|
| 20 |
"@monaco-editor/loader": "^1.5.0",
|
| 21 |
"@tailwindcss/vite": "^4.0.14",
|
| 22 |
"dotenv": "^16.4.7",
|
src/services/repoApi.ts
CHANGED
|
@@ -30,6 +30,25 @@ interface IRepoApi {
|
|
| 30 |
updateFile(account: Account, path: string, content: string, sha: string, message?: string): Promise<any>;
|
| 31 |
deleteFile(account: Account, path: string, sha: string, message?: string): Promise<any>;
|
| 32 |
createFile(account: Account, path: string, content: string, message?: string): Promise<any>;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
|
| 35 |
// GitHub implementation
|
|
@@ -60,7 +79,6 @@ class GitHubRepoApi implements IRepoApi {
|
|
| 60 |
}
|
| 61 |
|
| 62 |
async updateFile(account: Account, path: string, content: string, sha: string, message?: string) {
|
| 63 |
-
|
| 64 |
const encoder = new TextEncoder();
|
| 65 |
const bytes = encoder.encode(content);
|
| 66 |
const base64Content = btoa(String.fromCharCode.apply(null, [...new Uint8Array(bytes)]));
|
|
@@ -95,19 +113,29 @@ class GitHubRepoApi implements IRepoApi {
|
|
| 95 |
}
|
| 96 |
|
| 97 |
async createFile(account: Account, path: string, content: string, message?: string) {
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
const response = await fetch(
|
| 112 |
`${API_BASE_URL}/api/github/${account.owner}/${account.repo}/${path}`,
|
| 113 |
{
|
|
@@ -125,7 +153,6 @@ class GitHubRepoApi implements IRepoApi {
|
|
| 125 |
|
| 126 |
// HuggingFace implementation
|
| 127 |
class HuggingFaceRepoApi implements IRepoApi {
|
| 128 |
-
|
| 129 |
async getContents(account: Account, path: string = ''): Promise<RepoContent[]> {
|
| 130 |
const response = await fetch(
|
| 131 |
`${API_BASE_URL}/api/hf/${account.owner}/${account.repo}/tree/${account.ref}/${path}`,
|
|
@@ -141,13 +168,14 @@ class HuggingFaceRepoApi implements IRepoApi {
|
|
| 141 |
return result.map((item: any) => {
|
| 142 |
item.sha = item.oid;
|
| 143 |
item.name = item.path.split('/').pop() || '';
|
|
|
|
| 144 |
return item;
|
| 145 |
});
|
| 146 |
}
|
| 147 |
|
| 148 |
async getFileContent(account: Account, path: string): Promise<RepoContent> {
|
| 149 |
-
const
|
| 150 |
-
|
| 151 |
{
|
| 152 |
method: 'GET',
|
| 153 |
headers: {
|
|
@@ -163,6 +191,24 @@ class HuggingFaceRepoApi implements IRepoApi {
|
|
| 163 |
throw new Error('认证失败,请重新登录');
|
| 164 |
}
|
| 165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
return {
|
| 167 |
content: await response.text(),
|
| 168 |
encoding: 'utf-8',
|
|
@@ -188,15 +234,10 @@ class HuggingFaceRepoApi implements IRepoApi {
|
|
| 188 |
Authorization: `Bearer ${account.token}`
|
| 189 |
},
|
| 190 |
body: JSON.stringify({
|
| 191 |
-
"
|
| 192 |
-
"
|
| 193 |
-
"
|
| 194 |
-
|
| 195 |
-
"content": content,
|
| 196 |
-
"encoding": "utf-8",
|
| 197 |
-
"path": path
|
| 198 |
-
}
|
| 199 |
-
]
|
| 200 |
})
|
| 201 |
}
|
| 202 |
);
|
|
@@ -218,13 +259,9 @@ class HuggingFaceRepoApi implements IRepoApi {
|
|
| 218 |
Authorization: `Bearer ${account.token}`
|
| 219 |
},
|
| 220 |
body: JSON.stringify({
|
| 221 |
-
"
|
| 222 |
-
"
|
| 223 |
-
"
|
| 224 |
-
{
|
| 225 |
-
"path": path
|
| 226 |
-
}
|
| 227 |
-
]
|
| 228 |
})
|
| 229 |
}
|
| 230 |
);
|
|
@@ -241,15 +278,36 @@ class HuggingFaceRepoApi implements IRepoApi {
|
|
| 241 |
Authorization: `Bearer ${account.token}`
|
| 242 |
},
|
| 243 |
body: JSON.stringify({
|
| 244 |
-
"
|
| 245 |
-
"
|
| 246 |
-
"
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
})
|
| 254 |
}
|
| 255 |
);
|
|
@@ -310,5 +368,10 @@ export const repoApi = {
|
|
| 310 |
async createFile(account: Account, path: string, content: string, message?: string) {
|
| 311 |
const api = RepoApiFactory.getRepoApi(account.type);
|
| 312 |
return api.createFile(account, path, content, message);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
}
|
| 314 |
};
|
|
|
|
| 30 |
updateFile(account: Account, path: string, content: string, sha: string, message?: string): Promise<any>;
|
| 31 |
deleteFile(account: Account, path: string, sha: string, message?: string): Promise<any>;
|
| 32 |
createFile(account: Account, path: string, content: string, message?: string): Promise<any>;
|
| 33 |
+
createAsset(account: Account, path: string, content: Uint8Array, message?: string): Promise<any>;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// 浏览器环境中将 ArrayBuffer/Uint8Array 转换为 Base64 的辅助方法
|
| 37 |
+
function arrayBufferToBase64(buffer: Uint8Array): Promise<string> {
|
| 38 |
+
return new Promise((resolve, reject) => {
|
| 39 |
+
const blob = new Blob([buffer]);
|
| 40 |
+
const reader = new FileReader();
|
| 41 |
+
|
| 42 |
+
reader.onload = () => {
|
| 43 |
+
const dataUrl = reader.result as string;
|
| 44 |
+
// 移除 data URL 前缀 (例如 "data:image/png;base64,")
|
| 45 |
+
const base64 = dataUrl.split(',')[1];
|
| 46 |
+
resolve(base64);
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
reader.onerror = reject;
|
| 50 |
+
reader.readAsDataURL(blob);
|
| 51 |
+
});
|
| 52 |
}
|
| 53 |
|
| 54 |
// GitHub implementation
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
async updateFile(account: Account, path: string, content: string, sha: string, message?: string) {
|
|
|
|
| 82 |
const encoder = new TextEncoder();
|
| 83 |
const bytes = encoder.encode(content);
|
| 84 |
const base64Content = btoa(String.fromCharCode.apply(null, [...new Uint8Array(bytes)]));
|
|
|
|
| 113 |
}
|
| 114 |
|
| 115 |
async createFile(account: Account, path: string, content: string, message?: string) {
|
| 116 |
+
const encoder = new TextEncoder();
|
| 117 |
+
const bytes = encoder.encode(content);
|
| 118 |
+
const encodedContent = btoa(String.fromCharCode.apply(null, [...new Uint8Array(bytes)]));
|
| 119 |
|
| 120 |
+
const response = await fetch(
|
| 121 |
+
`${API_BASE_URL}/api/github/${account.owner}/${account.repo}/${path}`,
|
| 122 |
+
{
|
| 123 |
+
method: 'POST',
|
| 124 |
+
headers: {
|
| 125 |
+
'Content-Type': 'application/json',
|
| 126 |
+
Authorization: `Bearer ${account.token}`
|
| 127 |
+
},
|
| 128 |
+
body: JSON.stringify({ branch: account.ref, content: encodedContent, message })
|
| 129 |
+
}
|
| 130 |
+
);
|
| 131 |
+
return handleResponse(response);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
async createAsset(account: Account, path: string, content: Uint8Array, message?: string) {
|
| 137 |
+
|
| 138 |
+
const encodedContent = await arrayBufferToBase64(content);
|
| 139 |
const response = await fetch(
|
| 140 |
`${API_BASE_URL}/api/github/${account.owner}/${account.repo}/${path}`,
|
| 141 |
{
|
|
|
|
| 153 |
|
| 154 |
// HuggingFace implementation
|
| 155 |
class HuggingFaceRepoApi implements IRepoApi {
|
|
|
|
| 156 |
async getContents(account: Account, path: string = ''): Promise<RepoContent[]> {
|
| 157 |
const response = await fetch(
|
| 158 |
`${API_BASE_URL}/api/hf/${account.owner}/${account.repo}/tree/${account.ref}/${path}`,
|
|
|
|
| 168 |
return result.map((item: any) => {
|
| 169 |
item.sha = item.oid;
|
| 170 |
item.name = item.path.split('/').pop() || '';
|
| 171 |
+
item.type = item.type === 'directory' ? 'dir' : 'file';
|
| 172 |
return item;
|
| 173 |
});
|
| 174 |
}
|
| 175 |
|
| 176 |
async getFileContent(account: Account, path: string): Promise<RepoContent> {
|
| 177 |
+
const url = `${API_BASE_URL}/api/hf/${account.owner}/${account.repo}/raw/${account.ref}/${path}`;
|
| 178 |
+
const response = await fetch(url,
|
| 179 |
{
|
| 180 |
method: 'GET',
|
| 181 |
headers: {
|
|
|
|
| 191 |
throw new Error('认证失败,请重新登录');
|
| 192 |
}
|
| 193 |
|
| 194 |
+
|
| 195 |
+
//根据响应的内容类型设置encoding
|
| 196 |
+
const contentType = response.headers.get('content-type');
|
| 197 |
+
if (contentType && contentType.startsWith('image/')) {
|
| 198 |
+
return {
|
| 199 |
+
content: await arrayBufferToBase64(await response.bytes()),
|
| 200 |
+
download_url: null,
|
| 201 |
+
encoding: 'base64',
|
| 202 |
+
name: '',
|
| 203 |
+
path: path,
|
| 204 |
+
sha: '',
|
| 205 |
+
size: 0,
|
| 206 |
+
url: '',
|
| 207 |
+
html_url: '',
|
| 208 |
+
git_url: '',
|
| 209 |
+
type: 'file'
|
| 210 |
+
};
|
| 211 |
+
}
|
| 212 |
return {
|
| 213 |
content: await response.text(),
|
| 214 |
encoding: 'utf-8',
|
|
|
|
| 234 |
Authorization: `Bearer ${account.token}`
|
| 235 |
},
|
| 236 |
body: JSON.stringify({
|
| 237 |
+
"lfs": false,
|
| 238 |
+
"path": path,
|
| 239 |
+
"content": content,
|
| 240 |
+
"message": message,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
})
|
| 242 |
}
|
| 243 |
);
|
|
|
|
| 259 |
Authorization: `Bearer ${account.token}`
|
| 260 |
},
|
| 261 |
body: JSON.stringify({
|
| 262 |
+
"lfs": false,
|
| 263 |
+
"path": path,
|
| 264 |
+
"message": message,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
})
|
| 266 |
}
|
| 267 |
);
|
|
|
|
| 278 |
Authorization: `Bearer ${account.token}`
|
| 279 |
},
|
| 280 |
body: JSON.stringify({
|
| 281 |
+
"lfs": false,
|
| 282 |
+
"path": path,
|
| 283 |
+
"content": content,
|
| 284 |
+
"message": message,
|
| 285 |
+
})
|
| 286 |
+
}
|
| 287 |
+
);
|
| 288 |
+
const result = await handleResponse(response);
|
| 289 |
+
return {
|
| 290 |
+
content: {
|
| 291 |
+
sha: result.commitOid
|
| 292 |
+
}
|
| 293 |
+
};
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
async createAsset(account: Account, path: string, content: Uint8Array, message?: string) {
|
| 297 |
+
const encodedContent = await arrayBufferToBase64(content);
|
| 298 |
+
const response = await fetch(
|
| 299 |
+
`${API_BASE_URL}/api/hf/${account.owner}/${account.repo}/commit/${account.ref}/${path}`,
|
| 300 |
+
{
|
| 301 |
+
method: 'POST',
|
| 302 |
+
headers: {
|
| 303 |
+
'Content-Type': 'application/json',
|
| 304 |
+
Authorization: `Bearer ${account.token}`
|
| 305 |
+
},
|
| 306 |
+
body: JSON.stringify({
|
| 307 |
+
"lfs": true,
|
| 308 |
+
"path": path,
|
| 309 |
+
"content": encodedContent,
|
| 310 |
+
"message": message,
|
| 311 |
})
|
| 312 |
}
|
| 313 |
);
|
|
|
|
| 368 |
async createFile(account: Account, path: string, content: string, message?: string) {
|
| 369 |
const api = RepoApiFactory.getRepoApi(account.type);
|
| 370 |
return api.createFile(account, path, content, message);
|
| 371 |
+
},
|
| 372 |
+
|
| 373 |
+
async createAsset(account: Account, path: string, content: Uint8Array, message?: string) {
|
| 374 |
+
const api = RepoApiFactory.getRepoApi(account.type);
|
| 375 |
+
return api.createAsset(account, path, content, message);
|
| 376 |
}
|
| 377 |
};
|
src/views/ContentView.vue
CHANGED
|
@@ -77,10 +77,7 @@ const fetchContent = async () => {
|
|
| 77 |
if (isImageFile.value) {
|
| 78 |
if (result.content) {
|
| 79 |
// 如果有 content,将 base64 转换为 Blob URL
|
| 80 |
-
const
|
| 81 |
-
result.content :
|
| 82 |
-
btoa(result.content);
|
| 83 |
-
const byteCharacters = atob(base64Data);
|
| 84 |
const byteNumbers = new Array(byteCharacters.length);
|
| 85 |
for (let i = 0; i < byteCharacters.length; i++) {
|
| 86 |
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
@@ -353,6 +350,7 @@ const isImageFile = computed(() => {
|
|
| 353 |
watch(() => route.query, async (query) => {
|
| 354 |
// Only respond to query changes when on the repo route
|
| 355 |
if (route.path !== '/content') return;
|
|
|
|
| 356 |
const { id, path, newFile } = query;
|
| 357 |
isNewFile.value = !!newFile;
|
| 358 |
if (id) {
|
|
@@ -388,6 +386,7 @@ onUnmounted(() => {
|
|
| 388 |
onBeforeUnmount(() => {
|
| 389 |
if (imageUrl.value && imageUrl.value.startsWith('blob:')) {
|
| 390 |
URL.revokeObjectURL(imageUrl.value);
|
|
|
|
| 391 |
}
|
| 392 |
});
|
| 393 |
</script>
|
|
|
|
| 77 |
if (isImageFile.value) {
|
| 78 |
if (result.content) {
|
| 79 |
// 如果有 content,将 base64 转换为 Blob URL
|
| 80 |
+
const byteCharacters = atob(result.content);
|
|
|
|
|
|
|
|
|
|
| 81 |
const byteNumbers = new Array(byteCharacters.length);
|
| 82 |
for (let i = 0; i < byteCharacters.length; i++) {
|
| 83 |
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
|
|
| 350 |
watch(() => route.query, async (query) => {
|
| 351 |
// Only respond to query changes when on the repo route
|
| 352 |
if (route.path !== '/content') return;
|
| 353 |
+
imageUrl.value = '';
|
| 354 |
const { id, path, newFile } = query;
|
| 355 |
isNewFile.value = !!newFile;
|
| 356 |
if (id) {
|
|
|
|
| 386 |
onBeforeUnmount(() => {
|
| 387 |
if (imageUrl.value && imageUrl.value.startsWith('blob:')) {
|
| 388 |
URL.revokeObjectURL(imageUrl.value);
|
| 389 |
+
imageUrl.value = '';
|
| 390 |
}
|
| 391 |
});
|
| 392 |
</script>
|
src/views/UploadView.vue
CHANGED
|
@@ -94,23 +94,18 @@ const handleConfirmUpload = async () => {
|
|
| 94 |
throw new Error('Invalid file object');
|
| 95 |
}
|
| 96 |
|
| 97 |
-
const content = await new Promise<
|
| 98 |
const reader = new FileReader();
|
| 99 |
reader.onload = (e) => {
|
| 100 |
if (e.target?.result) {
|
| 101 |
-
|
| 102 |
-
const dataUrl = e.target.result.toString();
|
| 103 |
-
|
| 104 |
-
const base64 = dataUrl.split(',')[1];
|
| 105 |
-
|
| 106 |
-
resolve(base64);
|
| 107 |
} else {
|
| 108 |
reject(new Error('Failed to read file content'));
|
| 109 |
}
|
| 110 |
};
|
| 111 |
reader.onerror = () => reject(reader.error);
|
| 112 |
// 使用 readAsDataURL 来处理所有类型的文件
|
| 113 |
-
reader.
|
| 114 |
});
|
| 115 |
|
| 116 |
const filePath = currentPath.value
|
|
@@ -122,7 +117,7 @@ const handleConfirmUpload = async () => {
|
|
| 122 |
`上传文件: ${selectedFile.value.name}`;
|
| 123 |
|
| 124 |
// GitHub API 要求 base64 编码的内容
|
| 125 |
-
const response = await repoApi.
|
| 126 |
account,
|
| 127 |
filePath,
|
| 128 |
content,
|
|
|
|
| 94 |
throw new Error('Invalid file object');
|
| 95 |
}
|
| 96 |
|
| 97 |
+
const content = await new Promise<Uint8Array>((resolve, reject) => {
|
| 98 |
const reader = new FileReader();
|
| 99 |
reader.onload = (e) => {
|
| 100 |
if (e.target?.result) {
|
| 101 |
+
resolve(e.target.result as Uint8Array);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
} else {
|
| 103 |
reject(new Error('Failed to read file content'));
|
| 104 |
}
|
| 105 |
};
|
| 106 |
reader.onerror = () => reject(reader.error);
|
| 107 |
// 使用 readAsDataURL 来处理所有类型的文件
|
| 108 |
+
reader.readAsArrayBuffer(selectedFile.value as File);
|
| 109 |
});
|
| 110 |
|
| 111 |
const filePath = currentPath.value
|
|
|
|
| 117 |
`上传文件: ${selectedFile.value.name}`;
|
| 118 |
|
| 119 |
// GitHub API 要求 base64 编码的内容
|
| 120 |
+
const response = await repoApi.createAsset(
|
| 121 |
account,
|
| 122 |
filePath,
|
| 123 |
content,
|