Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/obsidian-plugin/release-notes/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
- Apply downloaded files in related groups and release their buffers promptly. Servers that report file sizes use memory-aware download admission; older servers retain parallel downloads. Oversized groups run on their own.

## Fixed

- Prevent files with incompatible paths from being uploaded, and identify blocked local files in the file explorer and blocked local or remote files in sync settings so they can be renamed safely. Preserve existing local files when a blocked remote rename would otherwise let another file overwrite them.
8 changes: 7 additions & 1 deletion apps/obsidian-plugin/src/app/plugin-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type {
SynchDeletedFilesPurgeResult,
SynchDeletedFilesRestoreResult,
SynchEntryVersionCursor,
SynchBlockedSyncFile,
SynchFileSizeBlockedFile,
SynchCommunityPluginUpdateStatus,
SynchServerCompatibilityStatus,
Expand Down Expand Up @@ -683,8 +684,13 @@ export class SynchPluginController implements SynchSettingsController {
return await this.versionHistoryController.listDeletedFiles(before, limit);
}

async listBlockedSyncFiles(): Promise<SynchBlockedSyncFile[]> {
return await this.syncController.listBlockedSyncFiles();
}

/** @deprecated Use `listBlockedSyncFiles`. */
async listFileSizeBlockedFiles(): Promise<SynchFileSizeBlockedFile[]> {
return await this.syncController.listFileSizeBlockedFiles();
return await this.listBlockedSyncFiles();
}

async previewDeletedFile(
Expand Down
7 changes: 4 additions & 3 deletions apps/obsidian-plugin/src/app/sync-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,8 @@ describe("SyncController", () => {
});

it("returns no file-size blocked files without an active authenticated remote vault session", async () => {
const listFileSizeBlockedFiles = vi
.spyOn(SyncEngine.prototype, "listFileSizeBlockedFiles")
const listBlockedSyncFiles = vi
.spyOn(SyncEngine.prototype, "listBlockedSyncFiles")
.mockResolvedValue([
{
path: "large.md",
Expand All @@ -411,8 +411,9 @@ describe("SyncController", () => {
}),
);

await expect(controller.listBlockedSyncFiles()).resolves.toEqual([]);
await expect(controller.listFileSizeBlockedFiles()).resolves.toEqual([]);
expect(listFileSizeBlockedFiles).not.toHaveBeenCalled();
expect(listBlockedSyncFiles).not.toHaveBeenCalled();
});
});

Expand Down
10 changes: 8 additions & 2 deletions apps/obsidian-plugin/src/app/sync-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
type SyncDeletedEntriesRestoreResult,
type SyncDeletedEntriesPurgeResult,
type SyncEngineEntryVersionsPage,
type SyncBlockedSyncFile,
type SyncFileSizeBlockedFile,
type SyncEntryVersionPreview,
getUserVisibleSyncDisplayPercent,
Expand Down Expand Up @@ -474,12 +475,17 @@ export class SyncController {
this.setSyncStatus("attention_needed");
}

async listFileSizeBlockedFiles(): Promise<SyncFileSizeBlockedFile[]> {
async listBlockedSyncFiles(): Promise<SyncBlockedSyncFile[]> {
if (!this.deps.hasActiveRemoteVaultSession() || !this.deps.hasAuthenticatedSession()) {
return [];
}

return await this.syncEngine.listFileSizeBlockedFiles();
return await this.syncEngine.listBlockedSyncFiles();
}

/** @deprecated Use `listBlockedSyncFiles`. */
async listFileSizeBlockedFiles(): Promise<SyncFileSizeBlockedFile[]> {
return await this.listBlockedSyncFiles();
}

async listEntryVersionsForPath(
Expand Down
2 changes: 2 additions & 0 deletions apps/obsidian-plugin/src/i18n/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ export const de = {
"sync.conflictLocalSaved": ({ path }: { path: string }) => `Sync-Konflikt erkannt. Ihre lokalen Änderungen wurden in „${path}“ gespeichert.`,
"sync.conflictRemoteKept": ({ path }: { path: string }) => `Sync-Konflikt für „${path}“ erkannt. Die Remote-Version wird behalten.`,
"sync.fileSizeBlocked": ({ count }: { count: number }) => `${count === 1 ? "1 Datei überschreitet" : `${count} Dateien überschreiten`} das Sync-Größenlimit.`,
"sync.incompatiblePathBlocked": "Der Pfad dieser Datei ist nicht kompatibel und kann nicht synchronisiert werden. Benennen Sie die Datei oder den Ordner um.",
"sync.incompatiblePathBlockedCount": ({ count }: { count: number }) => `${count === 1 ? "1 Datei hat" : `${count} Dateien haben`} einen inkompatiblen Pfad.`,
"sync.label": "Synchronisierung",
"sync.now": "Jetzt synchronisieren",
"sync.frequency": "Sync-Häufigkeit",
Expand Down
2 changes: 2 additions & 0 deletions apps/obsidian-plugin/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ export const en = {
"sync.conflictLocalSaved": ({ path }: { path: string }) => `Sync conflict detected. Your local changes were saved to "${path}".`,
"sync.conflictRemoteKept": ({ path }: { path: string }) => `Sync conflict detected for "${path}". The remote version will be kept.`,
"sync.fileSizeBlocked": ({ count }: { count: number }) => `${count} ${count === 1 ? "file exceeds" : "files exceed"} the sync size limit.`,
"sync.incompatiblePathBlocked": "Synch cannot sync this file because its path is incompatible. Rename the file or folder to continue.",
"sync.incompatiblePathBlockedCount": ({ count }: { count: number }) => `${count} ${count === 1 ? "file has" : "files have"} an incompatible path.`,
"sync.label": "Sync",
"sync.now": "Sync now",
"sync.frequency": "Sync frequency",
Expand Down
2 changes: 2 additions & 0 deletions apps/obsidian-plugin/src/i18n/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ export const ja = {
"sync.connectRemoteVault": "同期を開始するにはリモートvaultに接続してください。",
"sync.cursorMismatch": "このデバイスの同期履歴がリモートvaultと一致しないため、同期を停止しました。同期を再開するには、Synch設定でリモートvaultの接続を解除してから再接続してください。",
"sync.fileSizeBlocked": ({ count }: { count: number }) => `${count}件のファイルが同期サイズ制限を超えています。`,
"sync.incompatiblePathBlocked": "このファイルのパスには互換性がないため、Synch は同期できません。ファイル名またはフォルダ名を変更してください。",
"sync.incompatiblePathBlockedCount": ({ count }: { count: number }) => `${count}件のファイルに互換性のないパスがあります。`,
"sync.label": "同期",
"sync.now": "今すぐ同期",
"sync.frequency": "同期間隔",
Expand Down
2 changes: 2 additions & 0 deletions apps/obsidian-plugin/src/i18n/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ export const ko = {
"sync.connectRemoteVault": "동기화를 시작하려면 원격 vault에 연결하세요.",
"sync.cursorMismatch": "이 기기의 동기화 기록이 원격 vault와 일치하지 않아 동기화를 중지했습니다. 다시 동기화하려면 Synch 설정에서 원격 vault의 연결을 해제한 후 다시 연결하세요.",
"sync.fileSizeBlocked": ({ count }: { count: number }) => `${count}개 파일이 동기화 크기 제한을 초과했습니다.`,
"sync.incompatiblePathBlocked": "이 파일의 경로가 호환되지 않아 Synch가 동기화할 수 없습니다. 파일 또는 폴더 이름을 변경하세요.",
"sync.incompatiblePathBlockedCount": ({ count }: { count: number }) => `${count}개 파일의 경로가 호환되지 않습니다.`,
"sync.label": "동기화",
"sync.now": "지금 동기화",
"sync.frequency": "동기화 주기",
Expand Down
2 changes: 2 additions & 0 deletions apps/obsidian-plugin/src/i18n/locales/zh-cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ export const zhCn = {
"sync.connectRemoteVault": "连接远程 vault 以开始同步。",
"sync.cursorMismatch": "由于此设备的同步记录与远程 vault 不再一致,同步已暂停。要恢复同步,请在 Synch 设置中断开并重新连接远程 vault。",
"sync.fileSizeBlocked": ({ count }: { count: number }) => `${count} 个文件超出同步大小限制。`,
"sync.incompatiblePathBlocked": "此文件的路径不兼容,因此 Synch 无法同步。请重命名文件或文件夹。",
"sync.incompatiblePathBlockedCount": ({ count }: { count: number }) => `${count} 个文件的路径不兼容。`,
"sync.label": "同步",
"sync.now": "立即同步",
"sync.frequency": "同步频率",
Expand Down
2 changes: 2 additions & 0 deletions apps/obsidian-plugin/src/i18n/locales/zh-tw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ export const zhTw = {
"sync.connectRemoteVault": "連接遠端 vault 以開始同步。",
"sync.cursorMismatch": "由於此裝置的同步記錄與遠端 vault 不再一致,同步已暫停。若要恢復同步,請在 Synch 設定中中斷並重新連接遠端 vault。",
"sync.fileSizeBlocked": ({ count }: { count: number }) => `${count} 個檔案超出同步大小限制。`,
"sync.incompatiblePathBlocked": "此檔案的路徑不相容,因此 Synch 無法同步。請重新命名檔案或資料夾。",
"sync.incompatiblePathBlockedCount": ({ count }: { count: number }) => `${count} 個檔案的路徑不相容。`,
"sync.label": "同步",
"sync.now": "立即同步",
"sync.frequency": "同步頻率",
Expand Down
6 changes: 5 additions & 1 deletion apps/obsidian-plugin/src/ui/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,16 @@ export type SynchStorageDisplayState =
| "near_limit"
| "needs_more_storage";

export interface SynchFileSizeBlockedFile {
export interface SynchBlockedSyncFile {
path: string;
reason?: "file_too_large" | "incompatible_path";
encryptedSizeBytes: number | null;
maxFileSizeBytes: number | null;
}

/** @deprecated Use `SynchBlockedSyncFile`. */
export type SynchFileSizeBlockedFile = SynchBlockedSyncFile;

export type SynchCommunityPluginUpdateStatus =
| {
state: "idle" | "checking";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import type { Plugin } from "obsidian";
import { t } from "../../i18n";

import {
decorateFileExplorerElement,
Expand Down Expand Up @@ -106,7 +107,7 @@ describe("Synch file-size blocked decorator", () => {
registerEvent: () => {},
} as unknown as Plugin,
{
async listFileSizeBlockedFiles() {
async listBlockedSyncFiles() {
return [
{
path: "large.md",
Expand Down Expand Up @@ -138,6 +139,15 @@ describe("Synch file-size blocked decorator", () => {
}).not.toThrow();
expect(tooltip.length).toBeGreaterThan(0);
});

it("explains when a file path is incompatible", () => {
expect(formatFileSizeBlockedTooltip({
path: "Notes/a:b.md",
reason: "incompatible_path",
encryptedSizeBytes: null,
maxFileSizeBytes: null,
})).toBe(t("sync.incompatiblePathBlocked"));
});
});

function createFileExplorerRoot(paths: string[]): FakeElement {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { setIcon, setTooltip, type Plugin } from "obsidian";

import { getSynchLocale } from "../../i18n";
import type { SynchFileSizeBlockedFile } from "../contracts";
import { getSynchLocale, t } from "../../i18n";
import type { SynchBlockedSyncFile } from "../contracts";

const FILE_EXPLORER_VIEW_TYPE = "file-explorer";
const FILE_TITLE_SELECTOR = ".nav-file-title[data-path]";
const BLOCKED_CLASS = "synch-file-size-blocked";
const ICON_CLASS = "synch-file-size-blocked-icon";

export interface SynchFileSizeBlockedDecoratorState {
listFileSizeBlockedFiles(): Promise<SynchFileSizeBlockedFile[]>;
listBlockedSyncFiles(): Promise<SynchBlockedSyncFile[]>;
}

export class SynchFileSizeBlockedDecorator {
private refreshTimer: number | null = null;
private refreshRun = 0;
private blockedFiles: SynchFileSizeBlockedFile[] = [];
private blockedFiles: SynchBlockedSyncFile[] = [];

constructor(
private readonly plugin: Plugin,
Expand Down Expand Up @@ -54,7 +54,7 @@ export class SynchFileSizeBlockedDecorator {
this.refreshRun = run;

try {
const blockedFiles = await this.state.listFileSizeBlockedFiles();
const blockedFiles = await this.state.listBlockedSyncFiles();
if (run !== this.refreshRun) {
return;
}
Expand All @@ -71,7 +71,7 @@ export class SynchFileSizeBlockedDecorator {
}
}

private decorate(blockedFiles: SynchFileSizeBlockedFile[]): void {
private decorate(blockedFiles: SynchBlockedSyncFile[]): void {
const blockedByPath = new Map(blockedFiles.map((file) => [file.path, file]));
for (const leaf of this.plugin.app.workspace.getLeavesOfType(FILE_EXPLORER_VIEW_TYPE)) {
decorateFileExplorerElement(leaf.view.containerEl, blockedByPath);
Expand All @@ -81,7 +81,7 @@ export class SynchFileSizeBlockedDecorator {

export function decorateFileExplorerElement(
root: HTMLElement,
blockedByPath: ReadonlyMap<string, SynchFileSizeBlockedFile>,
blockedByPath: ReadonlyMap<string, SynchBlockedSyncFile>,
): void {
for (const icon of queryHtmlElements(root, `.${ICON_CLASS}`)) {
icon.remove();
Expand Down Expand Up @@ -116,7 +116,11 @@ function queryHtmlElements(root: HTMLElement, selector: string): HTMLElement[] {
return Array.from(elements);
}

export function formatFileSizeBlockedTooltip(file: SynchFileSizeBlockedFile): string {
export function formatFileSizeBlockedTooltip(file: SynchBlockedSyncFile): string {
if (file.reason === "incompatible_path") {
return t("sync.incompatiblePathBlocked");
}

switch (getSynchLocale()) {
case "ko":
return [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export function createSettingsTab(
}),
clearSyncLogs: vi.fn(() => {}),
subscribeSyncLogs: () => () => {},
listBlockedSyncFiles: vi.fn(async () => []),
listFileSizeBlockedFiles: vi.fn(async () => []),
isSyncEnabled: () => true,
setSyncEnabled: vi.fn(async () => {}),
Expand Down
3 changes: 3 additions & 0 deletions apps/obsidian-plugin/src/ui/settings/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
SynchDeletedFilesPurgeResult,
SynchDeletedFile,
SynchDeletedFilesRestoreResult,
SynchBlockedSyncFile,
SynchFileSizeBlockedFile,
SynchCommunityPluginUpdateStatus,
SynchServerCompatibilityStatus,
Expand Down Expand Up @@ -36,6 +37,8 @@ export interface SynchSettingsController {
getSyncLogs(): SynchSyncLogs;
clearSyncLogs(): void;
subscribeSyncLogs(listener: () => void): () => void;
listBlockedSyncFiles(): Promise<SynchBlockedSyncFile[]>;
/** @deprecated Use `listBlockedSyncFiles`. */
listFileSizeBlockedFiles(): Promise<SynchFileSizeBlockedFile[]>;
isSyncEnabled(): boolean;
setSyncEnabled(enabled: boolean): Promise<void>;
Expand Down
26 changes: 19 additions & 7 deletions apps/obsidian-plugin/src/ui/settings/sections/sync-status.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { setIcon, setTooltip, Setting } from "obsidian";
import { t } from "../../../i18n";
import type { SynchSettingsController } from "../controller";
import type { SynchStorageDisplayState } from "../../contracts";
import type { SynchBlockedSyncFile, SynchStorageDisplayState } from "../../contracts";
import { formatStorageDescription, formatSyncDescription, getStoragePercent, shouldShowSyncSpinner } from "../format";
import {
FileSizeBlockedWarningControls,
Expand Down Expand Up @@ -159,9 +159,9 @@ function createFileSizeBlockedWarningControls(
let icon: HTMLElement | null = null;

async function refresh(currentRun: number): Promise<void> {
let blockedFileCount = 0;
let blockedFiles: SynchBlockedSyncFile[];
try {
blockedFileCount = (await controller.listFileSizeBlockedFiles()).length;
blockedFiles = await controller.listBlockedSyncFiles();
} catch {
return;
}
Expand All @@ -171,7 +171,7 @@ function createFileSizeBlockedWarningControls(

icon?.remove();
icon = null;
if (blockedFileCount <= 0) {
if (blockedFiles.length === 0) {
return;
}

Expand All @@ -180,7 +180,7 @@ function createFileSizeBlockedWarningControls(
});
icon.setAttribute("aria-hidden", "true");
setIcon(icon, "triangle-alert");
setTooltip(icon, formatFileSizeBlockedTooltip(blockedFileCount), {
setTooltip(icon, formatBlockedSyncTooltip(blockedFiles), {
delay: 1,
placement: "right",
});
Expand All @@ -194,6 +194,18 @@ function createFileSizeBlockedWarningControls(
};
}

function formatFileSizeBlockedTooltip(blockedFileCount: number): string {
return t("sync.fileSizeBlocked", { count: blockedFileCount });
function formatBlockedSyncTooltip(blockedFiles: SynchBlockedSyncFile[]): string {
const incompatiblePathCount = blockedFiles.filter(
(file) => file.reason === "incompatible_path",
).length;
const fileSizeCount = blockedFiles.length - incompatiblePathCount;

return [
fileSizeCount > 0 ? t("sync.fileSizeBlocked", { count: fileSizeCount }) : null,
incompatiblePathCount > 0
? t("sync.incompatiblePathBlockedCount", { count: incompatiblePathCount })
: null,
]
.filter((message): message is string => message !== null)
.join(" ");
}
Original file line number Diff line number Diff line change
Expand Up @@ -393,17 +393,25 @@ describe("SynchSettingTab sync status", () => {
completedEntries: 4000,
totalEntries: 4001,
}),
listFileSizeBlockedFiles: vi.fn(async () => [
listBlockedSyncFiles: vi.fn(async () => [
{
path: "large.bin",
reason: "file_too_large",
encryptedSizeBytes: 12_400_000,
maxFileSizeBytes: 10_000_000,
},
{
path: "larger.bin",
reason: "file_too_large",
encryptedSizeBytes: 22_400_000,
maxFileSizeBytes: 10_000_000,
},
{
path: "notes/a:b.md",
reason: "incompatible_path",
encryptedSizeBytes: null,
maxFileSizeBytes: null,
},
]),
});

Expand All @@ -418,7 +426,7 @@ describe("SynchSettingTab sync status", () => {
attributes: expect.objectContaining({
"aria-hidden": "true",
"data-icon": "triangle-alert",
"data-tooltip": t("sync.fileSizeBlocked", { count: 2 }),
"data-tooltip": `${t("sync.fileSizeBlocked", { count: 2 })} ${t("sync.incompatiblePathBlockedCount", { count: 1 })}`,
"data-tooltip-delay": "1",
"data-tooltip-placement": "right",
}),
Expand All @@ -427,12 +435,12 @@ describe("SynchSettingTab sync status", () => {
});

it("refreshes the file size warning without rerendering the settings tab", async () => {
let blockedFiles: Awaited<ReturnType<SynchSettingsController["listFileSizeBlockedFiles"]>> = [];
let blockedFiles: Awaited<ReturnType<SynchSettingsController["listBlockedSyncFiles"]>> = [];
const tab = createSettingsTab({
hasAuthenticatedSession: () => true,
hasConnectedRemoteVault: () => true,
getSyncState: () => "up_to_date",
listFileSizeBlockedFiles: vi.fn(async () => blockedFiles),
listBlockedSyncFiles: vi.fn(async () => blockedFiles),
});

tab.open();
Expand Down
Loading
Loading