Files
obsidian-remarkable/src/sync/downloader.ts
T
fegger 49278723b1 Update build system and path handling
- Use Node filesystem APIs instead of shell commands for temp cleanup
- Add absolute path helpers for cross-platform compatibility
- Generate both main.js and dist/main.js artifacts
- Improve error handling in document downloader
- Track additional document metadata in sync tracker
- Replace Unix-specific `find` with recursive directory traversal

The build system now generates two output files (main.js and dist/main.js) for
better compatibility with Obsidian's plugin loading. Path handling has been
centralized in new utilities to ensure cross-platform behavior, replacing shell
commands that had Windows compatibility issues.
2026-05-31 16:01:44 +02:00

112 lines
3.5 KiB
TypeScript

import { RmapiNode } from "../types";
import RemarkablePlugin from "../main";
import { Notice } from "obsidian";
import { vaultPathToAbsolute, vaultRelativePath } from "../utils/paths";
function sanitizeFileName(name: string): string {
return name.replace(/[\\/:*?"<>|]/g, "_");
}
function sanitizeRemotePath(remotePath: string): string[] {
return remotePath.split("/").filter(Boolean).map(sanitizeFileName);
}
interface DocumentEntry {
node: RmapiNode;
remotePath: string;
}
export class DocumentDownloader {
plugin: RemarkablePlugin;
constructor(plugin: RemarkablePlugin) {
this.plugin = plugin;
}
async syncAll(): Promise<void> {
try {
new Notice("Syncing from reMarkable...");
const allDocs = await this.listAllDocuments("/");
for (const entry of allDocs) {
await this.downloadDocument(entry);
}
new Notice(`Sync completed! ${allDocs.length} document(s) synced.`);
} catch (e) {
new Notice(`Sync failed: ${e}`);
console.error("Sync error:", e);
throw e;
}
}
async listAllDocuments(path: string): Promise<DocumentEntry[]> {
const nodes = await this.plugin.rmapi.list(path);
let docs: DocumentEntry[] = nodes
.filter((n) => n.type === "DocumentType")
.map((n) => ({
node: n,
remotePath: path === "/" ? `/${n.name}` : `${path}/${n.name}`,
}));
for (const col of nodes.filter((n) => n.type === "CollectionType")) {
const subPath = path === "/" ? `/${col.name}` : `${path}/${col.name}`;
const subDocs = await this.listAllDocuments(subPath);
docs = docs.concat(subDocs);
}
return docs;
}
async downloadDocument(entry: DocumentEntry): Promise<void> {
const { node: doc, remotePath } = entry;
const synced = await this.plugin.tracker.getSyncedDocument(doc.id);
if (synced && synced.remoteVersion === doc.version && synced.remoteModified === doc.modifiedClient) {
return;
}
const pathSegments = sanitizeRemotePath(remotePath);
const safeName = pathSegments.pop() || sanitizeFileName(doc.name);
const localDir = vaultRelativePath(this.plugin.settings.downloadPath, ...pathSegments);
const localPath = vaultRelativePath(localDir, `${safeName}.rm`);
const absoluteLocalPath = vaultPathToAbsolute(this.plugin, localPath);
await this.ensureVaultFolder(localDir);
await this.plugin.rmapi.downloadFile(remotePath, absoluteLocalPath);
let hasPdf = false;
if (this.plugin.settings.convertToPdf) {
const pdfPath = vaultRelativePath(localDir, `${safeName}.pdf`);
await this.plugin.rmapi.downloadAnnotatedPdf(remotePath, vaultPathToAbsolute(this.plugin, pdfPath));
hasPdf = true;
}
await this.plugin.tracker.trackDocument(doc, localPath, hasPdf, false);
if (this.plugin.settings.enableHandwritingMd) {
const mdPath = vaultRelativePath(localDir, `${safeName}.md`);
const mdResult = await this.plugin.ocrPipeline.processDocument(localPath, mdPath);
if (mdResult) {
await this.plugin.tracker.trackDocument(doc, localPath, hasPdf, true);
}
}
}
private async ensureVaultFolder(path: string): Promise<void> {
const segments = path.split("/").filter(Boolean);
let current = "";
for (const segment of segments) {
current = current ? vaultRelativePath(current, segment) : segment;
try {
await this.plugin.app.vault.adapter.mkdir(current);
} catch (err) {
if (!(await this.plugin.app.vault.adapter.exists(current))) {
throw err;
}
}
}
}
}