Skip to content

Commit dcfd8e0

Browse files
authored
fix: load sheets in subdirectory from Codeberg VFS (#189)
1 parent 54eddb8 commit dcfd8e0

5 files changed

Lines changed: 85 additions & 75 deletions

File tree

‎src/kicanvas/project.ts‎

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,42 +54,39 @@ export class Project extends EventTarget implements IDisposable {
5454

5555
// 'Recursively' resolve all schematics until none remain
5656
let load_new = true;
57-
const skipped_files: string[] = [];
57+
const loaded_files: string[] = [];
5858
while (load_new) {
5959
load_new = false;
6060

61-
const loaded_file = Array.from(this.schematics());
62-
for (const sch of loaded_file) {
61+
const pending = [];
62+
for (const sch of this.schematics()) {
6363
const base_dir = dirname(sch.filename);
6464
for (const subsch of sch.sheets) {
6565
if (!subsch.sheetfile) {
6666
continue;
6767
}
6868

6969
const new_file = normalize_join(base_dir, subsch.sheetfile);
70-
71-
const loaded = loaded_file.map((s) => s.filename);
72-
if (
73-
loaded.includes(new_file) ||
74-
skipped_files.includes(new_file)
75-
) {
70+
if (loaded_files.includes(new_file)) {
7671
// file loaded or skipped
7772
continue;
7873
}
7974

8075
load_new = true;
76+
loaded_files.push(new_file);
8177

8278
if (await this.#fs.has(new_file)) {
8379
// load file, it changes this.#files_by_name and causes calling
8480
// this.schematics() will return a new result.
85-
await this.#load_file(new_file);
81+
pending.push(this.#load_file(new_file));
8682
} else {
8783
// skip non-existent files to allow loading an incomplete schematics
88-
skipped_files.push(new_file);
8984
log.warn(`file "${new_file}" is not existed, skip it.`);
9085
}
9186
}
9287
}
88+
89+
await Promise.all(pending);
9390
}
9491

9592
this.#determine_schematic_hierarchy();

‎src/kicanvas/services/codeberg-vfs.ts‎

Lines changed: 63 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,52 @@
44
Full text available at: https://opensource.org/licenses/MIT
55
*/
66
import { base64_decode } from "../../base/base64";
7-
import { initiate_download } from "../../base/dom/download";
8-
import { extension } from "../../base/paths";
9-
import { Codeberg, GetBlobResponse, RepoContentResponse } from "./codeberg";
10-
import { type IFileSystem } from "./vfs";
7+
import { based_on, normalize_join } from "../../base/paths";
8+
import {
9+
Codeberg,
10+
CodebergRepoInfo,
11+
GetBlobResponse,
12+
RepoContentResponse,
13+
} from "./codeberg";
14+
import { FileEntry, FileSystemBase } from "./vfs";
15+
16+
export class CodebergFileSystem extends FileSystemBase {
17+
private download_urls: Map<string, URL> = new Map();
18+
19+
constructor(private repo: CodebergRepoInfo) {
20+
super();
21+
}
1122

12-
export class CodebergFileSystem implements IFileSystem {
13-
static readonly kicad_extensions = ["kicad_pcb", "kicad_pro", "kicad_sch"];
23+
async load_file(path: string): Promise<File> {
24+
const url = this.download_urls.get(path);
25+
if (!url) {
26+
throw new Error(`File ${path} not found.`);
27+
}
1428

15-
async setup() {}
16-
constructor(private files_to_urls: Map<string, URL>) {}
29+
// API: https://codeberg.org/api/swagger#/repository/GetBlob
30+
const blob = await Codeberg.request_json<GetBlobResponse>(url.pathname);
1731

18-
public static async fromURLs(
19-
url: string | URL,
20-
): Promise<CodebergFileSystem | null> {
21-
const files_to_urls = new Map<string, URL>();
32+
if (blob.content.length === 0) {
33+
throw new Error(`Blob returns empty when loading file ${path}`);
34+
}
2235

23-
const info = Codeberg.parse_url(url);
24-
if (!info) {
25-
return null;
36+
if (blob.encoding !== "base64") {
37+
throw new Error(`Unsupported encoding: ${blob.encoding}`);
2638
}
2739

40+
const content = base64_decode(blob.content);
41+
const file = new File([content], path);
42+
43+
return file;
44+
}
45+
46+
async enumerate(cur_path: string): Promise<FileEntry[]> {
2847
// API:
2948
// https://codeberg.org/api/swagger#/repository/repoGetContents
30-
const api_url = `repos/${info.owner}/${info.repo}/contents/${info.path}`;
49+
const info = this.repo;
50+
const base_dir = info.path;
51+
const base_api = `repos/${info.owner}/${info.repo}/contents`;
52+
const api_url = normalize_join(base_api, base_dir, cur_path);
3153

3254
let files = await Codeberg.request_json<
3355
RepoContentResponse | RepoContentResponse[]
@@ -37,58 +59,39 @@ export class CodebergFileSystem implements IFileSystem {
3759
files = [files];
3860
}
3961

40-
for (const file of files) {
41-
if (!file.name || !file.git_url || file.type !== "file") {
42-
continue;
43-
}
44-
62+
const result: FileEntry[] = [];
63+
for (const it of files) {
4564
if (
46-
!CodebergFileSystem.kicad_extensions.includes(
47-
extension(file.name),
48-
)
65+
it.type === "file" &&
66+
CodebergFileSystem.is_kicad_file(it.name)
4967
) {
50-
continue;
68+
const file_path = based_on(base_dir, it.path);
69+
70+
this.download_urls.set(file_path, new URL(it.git_url));
71+
72+
result.push({
73+
type: "file",
74+
path: file_path,
75+
});
76+
} else if (it.type === "dir") {
77+
result.push({
78+
type: "directory",
79+
path: based_on(base_dir, it.path),
80+
});
5181
}
52-
53-
files_to_urls.set(file.name, new URL(file.git_url));
54-
}
55-
56-
if (files_to_urls.size == 0) {
57-
// no valid URL and files, return null.
58-
return null;
5982
}
6083

61-
return new CodebergFileSystem(files_to_urls);
62-
}
63-
64-
*list(): Generator<string> {
65-
yield* this.files_to_urls.keys();
84+
return result;
6685
}
6786

68-
async get(name: string) {
69-
const url = this.files_to_urls.get(name);
70-
if (!url) {
71-
throw new Error(`File ${name} not found.`);
72-
}
73-
74-
// API: https://codeberg.org/api/swagger#/repository/GetBlob
75-
const blob = await Codeberg.request_json<GetBlobResponse>(url.pathname);
76-
77-
if (blob.encoding !== "base64") {
78-
throw new Error(`Unsupported encoding: ${blob.encoding}`);
87+
public static async fromURLs(
88+
url: string | URL,
89+
): Promise<CodebergFileSystem | null> {
90+
const info = Codeberg.parse_url(url);
91+
if (!info) {
92+
return null;
7993
}
8094

81-
const content = base64_decode(blob.content);
82-
const file = new File([content], name);
83-
84-
return file;
85-
}
86-
87-
async has(name: string) {
88-
return Promise.resolve(this.files_to_urls.has(name));
89-
}
90-
91-
async download(name: string) {
92-
initiate_download(await this.get(name));
95+
return new CodebergFileSystem(info);
9396
}
9497
}

‎src/kicanvas/services/codeberg.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,20 @@ export class GetBlobResponse {
2222
url: string;
2323
}
2424

25+
export class CodebergRepoInfo {
26+
owner: string;
27+
repo: string;
28+
ref: string;
29+
path: string;
30+
}
31+
2532
export class Codeberg {
2633
static readonly host_name = "codeberg.org";
2734
static readonly html_base_url = "https://codeberg.org/";
2835
static readonly base_url = "https://codeberg.org/api/v1/";
2936
static readonly accept_header = "application/json";
3037

31-
static parse_url(url: string | URL) {
38+
static parse_url(url: string | URL): CodebergRepoInfo | null {
3239
url = new URL(url, Codeberg.html_base_url);
3340
if (url.hostname !== Codeberg.host_name) {
3441
return null;
@@ -41,6 +48,9 @@ export class Codeberg {
4148
}
4249

4350
const [, owner, repo, ...parts] = path_parts;
51+
if (!owner || !repo) {
52+
return null;
53+
}
4454

4555
let ref, path;
4656
if (parts.length > 0) {

‎src/kicanvas/services/github-vfs.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class GitHubFileSystem extends FileSystemBase {
4242
}
4343
}
4444

45-
override async load_file(path: string): Promise<File> {
45+
async load_file(path: string): Promise<File> {
4646
const download_url = this.download_urls.get(path);
4747
if (!download_url) {
4848
throw new Error(`File ${path} not found!`);
@@ -51,7 +51,7 @@ export class GitHubFileSystem extends FileSystemBase {
5151
return await gh_user_content.get(download_url);
5252
}
5353

54-
override async enumerate(cur_dir: string): Promise<FileEntry[]> {
54+
async enumerate(cur_dir: string): Promise<FileEntry[]> {
5555
if (this.single_file) {
5656
// single file, return all files directly
5757
return Array.from(this.download_urls.keys()).map((v) => ({

‎src/kicanvas/services/vfs.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export interface IFileSystem {
2424
/** List all files */
2525
list(): Generator<string>;
2626

27-
/** Initialize it */
27+
/** Initialize it. Call this function befoce using VFS */
2828
setup(): Promise<void>;
2929

3030
/** Get a file */

0 commit comments

Comments
 (0)