mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-16 14:22:32 +00:00
Report incomplete Object Storage wipes
This commit is contained in:
+5
-1
@@ -1113,7 +1113,11 @@ Purge all download/upload cache.
|
|||||||
|
|
||||||
#### Fresh Start Wipe
|
#### Fresh Start Wipe
|
||||||
|
|
||||||
Delete all data on the remote server.
|
Delete all data on the remote server in batches; this operation is not
|
||||||
|
transactional. Stop all synchronising devices before starting. If the
|
||||||
|
operation is interrupted or reports failure, keep them stopped, rerun Fresh
|
||||||
|
Start Wipe, and then use **Overwrite Server Data with This Device's Files**
|
||||||
|
from an authoritative Vault.
|
||||||
|
|
||||||
### 6. Garbage Collection V3 (CouchDB only)
|
### 6. Garbage Collection V3 (CouchDB only)
|
||||||
|
|
||||||
|
|||||||
@@ -1209,8 +1209,17 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
new MinioStorageAdapter(this.core.settings, this.core)
|
new MinioStorageAdapter(this.core.settings, this.core)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
async resetRemoteBucket() {
|
/**
|
||||||
|
* Wipe the remote bucket through a short-lived Journal client.
|
||||||
|
* Journal wipes are batched and non-transactional, so a false result may
|
||||||
|
* leave a partial wipe which can be retried after all devices are stopped.
|
||||||
|
*/
|
||||||
|
async resetRemoteBucket(): Promise<boolean> {
|
||||||
const minioJournal = this.getMinioJournalSyncClient();
|
const minioJournal = this.getMinioJournalSyncClient();
|
||||||
await minioJournal.resetBucket();
|
try {
|
||||||
|
return await minioJournal.resetBucket();
|
||||||
|
} finally {
|
||||||
|
minioJournal.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,6 +237,26 @@ describe("ObsidianLiveSyncSettingTab connection testing", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ObsidianLiveSyncSettingTab Fresh Start Wipe", () => {
|
||||||
|
it("returns the remote wipe result and disposes its temporary Journal client", async () => {
|
||||||
|
const resetBucket = vi.fn(async () => false);
|
||||||
|
const dispose = vi.fn();
|
||||||
|
const tab = new ObsidianLiveSyncSettingTab(
|
||||||
|
{} as never,
|
||||||
|
{
|
||||||
|
app: {},
|
||||||
|
core: {},
|
||||||
|
} as never
|
||||||
|
);
|
||||||
|
vi.spyOn(tab, "getMinioJournalSyncClient").mockReturnValue({ resetBucket, dispose } as never);
|
||||||
|
|
||||||
|
await expect(tab.resetRemoteBucket()).resolves.toBe(false);
|
||||||
|
|
||||||
|
expect(resetBucket).toHaveBeenCalledOnce();
|
||||||
|
expect(dispose).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("ObsidianLiveSyncSettingTab pending-setting initialisation", () => {
|
describe("ObsidianLiveSyncSettingTab pending-setting initialisation", () => {
|
||||||
function createSettingsTab() {
|
function createSettingsTab() {
|
||||||
const saveSettingData = vi.fn(async () => undefined);
|
const saveSettingData = vi.fn(async () => undefined);
|
||||||
|
|||||||
@@ -367,8 +367,13 @@ export function paneMaintenance(
|
|||||||
sentIDs: new Set(),
|
sentIDs: new Set(),
|
||||||
sentFiles: new Set(),
|
sentFiles: new Set(),
|
||||||
}));
|
}));
|
||||||
await this.resetRemoteBucket();
|
const reset = await this.resetRemoteBucket();
|
||||||
Logger(`Deleted all data on remote server`, LOG_LEVEL_NOTICE);
|
Logger(
|
||||||
|
reset
|
||||||
|
? `Deleted all data on remote server`
|
||||||
|
: `Fresh Start Wipe did not complete. Keep all synchronising devices stopped and run it again.`,
|
||||||
|
LOG_LEVEL_NOTICE
|
||||||
|
);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.addOnUpdate(this.onlyOnMinIO);
|
.addOnUpdate(this.onlyOnMinIO);
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const maintenanceHarness = vi.hoisted(() => ({
|
||||||
|
createdSettings: [] as Array<{ name: string; click?: () => Promise<void> }>,
|
||||||
|
logger: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/common/events.ts", () => ({
|
||||||
|
EVENT_REQUEST_PERFORM_GC_V3: "request-gc-v3",
|
||||||
|
eventHub: { emitEvent: vi.fn() },
|
||||||
|
}));
|
||||||
|
vi.mock("@/common/translation", () => ({
|
||||||
|
$msg: (message: string) => message,
|
||||||
|
}));
|
||||||
|
vi.mock("@/serviceFeatures/setupObsidian/settingsReset.ts", () => ({
|
||||||
|
createCoreSettingsAfterFullReset: vi.fn(),
|
||||||
|
createEditingSettingsAfterFullReset: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock("@vrtmrz/livesync-commonlib/compat/common/logger", () => ({
|
||||||
|
LOG_LEVEL_NOTICE: "notice",
|
||||||
|
Logger: maintenanceHarness.logger,
|
||||||
|
}));
|
||||||
|
vi.mock("@vrtmrz/livesync-commonlib/compat/common/types", () => ({
|
||||||
|
FlagFilesHumanReadable: {
|
||||||
|
FETCH_ALL: "fetch-all",
|
||||||
|
REBUILD_ALL: "rebuild-all",
|
||||||
|
},
|
||||||
|
FlagFilesOriginal: { SUSPEND_ALL: "suspend-all" },
|
||||||
|
}));
|
||||||
|
vi.mock("@vrtmrz/livesync-commonlib/compat/common/utils", () => ({
|
||||||
|
fireAndForget: (operation: Promise<unknown>) => operation,
|
||||||
|
}));
|
||||||
|
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator", () => ({
|
||||||
|
LiveSyncCouchDBReplicator: class {},
|
||||||
|
}));
|
||||||
|
vi.mock("./LiveSyncSetting.ts", () => ({
|
||||||
|
LiveSyncSetting: class {
|
||||||
|
name = "";
|
||||||
|
click?: () => Promise<void>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
maintenanceHarness.createdSettings.push(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDesc() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addButton(callback: (button: this) => void) {
|
||||||
|
callback(this);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setButtonText() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisabled() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCta() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(callback: () => Promise<void>) {
|
||||||
|
this.click = callback;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addOnUpdate() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.mock("./SettingPane", () => ({
|
||||||
|
visibleOnly: vi.fn(() => vi.fn()),
|
||||||
|
}));
|
||||||
|
vi.mock("./settingComponentStyles.ts", () => ({
|
||||||
|
setButtonDestructiveState: <T>(button: T) => button,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { paneMaintenance } from "./PaneMaintenance.ts";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
maintenanceHarness.createdSettings.length = 0;
|
||||||
|
maintenanceHarness.logger.mockClear();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("paneMaintenance Fresh Start Wipe", () => {
|
||||||
|
it("does not announce success when the remote wipe reports failure", async () => {
|
||||||
|
const updateCheckPointInfo = vi.fn(async () => undefined);
|
||||||
|
const resetRemoteBucket = vi.fn(async () => false);
|
||||||
|
const addPanel = vi.fn((_parent: HTMLElement, heading: string) => ({
|
||||||
|
then(callback: (paneEl: HTMLElement) => void) {
|
||||||
|
if (heading === "Rebuilding Operations (Remote Only)") {
|
||||||
|
callback({} as HTMLElement);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
const host = {
|
||||||
|
core: {
|
||||||
|
replicator: {},
|
||||||
|
storageAccess: {},
|
||||||
|
},
|
||||||
|
createEl: vi.fn(),
|
||||||
|
getMinioJournalSyncClient: vi.fn(() => ({ updateCheckPointInfo })),
|
||||||
|
onlyOnCouchDB: vi.fn(),
|
||||||
|
onlyOnCouchDBOrMinIO: vi.fn(),
|
||||||
|
onlyOnMinIO: vi.fn(),
|
||||||
|
resetRemoteBucket,
|
||||||
|
services: {
|
||||||
|
appLifecycle: { performRestart: vi.fn() },
|
||||||
|
database: { resetDatabase: vi.fn() },
|
||||||
|
databaseEvents: { initialiseDatabase: vi.fn() },
|
||||||
|
replication: { markLocked: vi.fn(), markUnlocked: vi.fn() },
|
||||||
|
setting: { saveSettingData: vi.fn() },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
paneMaintenance.call(host as never, {} as HTMLElement, { addPanel } as never);
|
||||||
|
const freshStartWipe = maintenanceHarness.createdSettings.find(({ name }) => name === "Fresh Start Wipe");
|
||||||
|
if (!freshStartWipe?.click) {
|
||||||
|
throw new Error("Fresh Start Wipe action was not registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
await freshStartWipe.click();
|
||||||
|
|
||||||
|
expect(resetRemoteBucket).toHaveBeenCalledOnce();
|
||||||
|
expect(maintenanceHarness.logger).toHaveBeenCalledWith(
|
||||||
|
"Fresh Start Wipe did not complete. Keep all synchronising devices stopped and run it again.",
|
||||||
|
"notice"
|
||||||
|
);
|
||||||
|
expect(maintenanceHarness.logger).not.toHaveBeenCalledWith("Deleted all data on remote server", "notice");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,6 +19,7 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
|
|||||||
- **Sync on Startup** now runs an immediate Object Storage synchronisation after start-up or resume, including migrated profiles which retain a Continuous setting that Object Storage cannot use.
|
- **Sync on Startup** now runs an immediate Object Storage synchronisation after start-up or resume, including migrated profiles which retain a Continuous setting that Object Storage cannot use.
|
||||||
- A temporarily unavailable Object Storage synchronisation-parameter read is no longer treated as a missing object and cannot regenerate the shared Security Seed. Flow-specific Security Seed checks also bypass an earlier process-cached result.
|
- A temporarily unavailable Object Storage synchronisation-parameter read is no longer treated as a missing object and cannot regenerate the shared Security Seed. Flow-specific Security Seed checks also bypass an earlier process-cached result.
|
||||||
- Local database reset and plug-in unload now retire active replication through its owner before closing the database, without reporting a missing active Replicator or describing unload as a database reset.
|
- Local database reset and plug-in unload now retire active replication through its owner before closing the database, without reporting a missing active Replicator or describing unload as a database reset.
|
||||||
|
- **Fresh Start Wipe** now reports an incomplete Object Storage deletion instead of announcing success, and releases its temporary storage client after each attempt.
|
||||||
|
|
||||||
### Peer-to-peer synchronisation
|
### Peer-to-peer synchronisation
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
|
|||||||
|
|
||||||
- The P2P Setup connection test no longer interrupts an active P2P room. It observes an active compatible relay binding, blocks a test which would add another relay until P2P is disconnected, and uses a short-lived trial only while P2P is idle.
|
- The P2P Setup connection test no longer interrupts an active P2P room. It observes an active compatible relay binding, blocks a test which would add another relay until P2P is disconnected, and uses a short-lived trial only while P2P is idle.
|
||||||
- Unattended P2P synchronisation no longer raises Notice-level messages for missing configured targets, authentication rejection, configuration mismatch, or an overlapping transfer. User-initiated operations retain their existing feedback.
|
- Unattended P2P synchronisation no longer raises Notice-level messages for missing configured targets, authentication rejection, configuration mismatch, or an overlapping transfer. User-initiated operations retain their existing feedback.
|
||||||
|
- P2P replication failure reasons now survive the JSON RPC boundary instead of reaching the requesting device as an empty object.
|
||||||
|
|
||||||
### Command-line interface
|
### Command-line interface
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user