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
5 changes: 3 additions & 2 deletions crates/client/src/sync/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ use super::local::{self, LocalScan, ScanError};
use super::path::RelPath;
use super::plan::{Action, reconcile};
use super::snapshot::Entry;
use super::state::{STATE_FILE_NAME, StateError, SyncState};
use super::state::{STATE_FILE_NAME, StateError, SyncState, adopt_legacy};
use super::transport::Transport;

const PARTIAL_SUFFIX: &str = ".roxypart";
const PARTIAL_SUFFIX: &str = ".stashpart";

#[derive(Debug, thiserror::Error)]
pub enum SyncError {
Expand Down Expand Up @@ -88,6 +88,7 @@ impl<T: Transport> Engine<T> {

pub fn open(root: impl Into<PathBuf>, transport: T) -> Result<Self, SyncError> {
let root = root.into();
adopt_legacy(&root)?;
let state = SyncState::load(&state_path(&root))?;
Ok(Self {
root,
Expand Down
6 changes: 4 additions & 2 deletions crates/client/src/sync/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use roxycloud_core::blob::BlobHash;

use super::path::RelPath;
use super::snapshot::Entry;
use super::state::{STATE_FILE_NAME, SyncState};
use super::state::{SyncState, is_state_file};

const READ_CHUNK: usize = 64 * 1024;

Expand Down Expand Up @@ -70,7 +70,7 @@ fn walk(
scan.skipped.push(entry.path());
continue;
};
if name == STATE_FILE_NAME {
if is_state_file(name) {
continue;
}

Expand Down Expand Up @@ -170,6 +170,7 @@ pub fn mtime_of(path: &Path) -> Option<i64> {
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::state::{LEGACY_STATE_FILE_NAME, STATE_FILE_NAME};

fn scratch(name: &str) -> PathBuf {
let directory = std::env::temp_dir().join(format!("roxycloud-scan-{name}"));
Expand Down Expand Up @@ -219,6 +220,7 @@ mod tests {
fn its_own_state_file_is_not_part_of_the_folder() {
let root = scratch("state-file");
write(&root, STATE_FILE_NAME, b"{}");
write(&root, LEGACY_STATE_FILE_NAME, b"{}");
write(&root, "a.txt", b"one");

let scan = scan(&root, &SyncState::default()).expect("scans");
Expand Down
2 changes: 1 addition & 1 deletion crates/client/src/sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ pub use local::{LocalScan, ScanError, scan};
pub use path::{InvalidRelPath, RelPath};
pub use plan::{Action, Plan, reconcile};
pub use snapshot::{Entry, Snapshot};
pub use state::{STATE_FILE_NAME, StateError, SyncState};
pub use state::{LEGACY_STATE_FILE_NAME, STATE_FILE_NAME, StateError, SyncState};
pub use transport::Transport;
pub use watch::{Command, Session, Status, WatchError, watch};
20 changes: 19 additions & 1 deletion crates/client/src/sync/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,25 @@ use serde::{Deserialize, Serialize};
use super::path::RelPath;
use super::snapshot::{Entry, Snapshot};

pub const STATE_FILE_NAME: &str = ".roxycloud-sync.json";
pub const STATE_FILE_NAME: &str = ".stashden-sync.json";
pub const LEGACY_STATE_FILE_NAME: &str = ".roxycloud-sync.json";

#[must_use]
pub fn is_state_file(name: &str) -> bool {
name == STATE_FILE_NAME || name == LEGACY_STATE_FILE_NAME
}

pub fn adopt_legacy(root: &Path) -> Result<(), StateError> {
let current = root.join(STATE_FILE_NAME);
let legacy = root.join(LEGACY_STATE_FILE_NAME);
if current.exists() || !legacy.exists() {
return Ok(());
}
fs::rename(&legacy, &current).map_err(|source| StateError::Write {
path: current,
source,
})
}

#[derive(Debug, thiserror::Error)]
pub enum StateError {
Expand Down
37 changes: 36 additions & 1 deletion crates/client/src/sync/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use super::held::Held;
use super::local;
use super::path::RelPath;
use super::snapshot::Snapshot;
use super::state::SyncState;
use super::state::{LEGACY_STATE_FILE_NAME, STATE_FILE_NAME, SyncState};
use super::transport::Transport;
use super::watch::{Command, Status, watch};

Expand Down Expand Up @@ -169,6 +169,41 @@ async fn a_file_deleted_locally_is_deleted_on_the_server() {
assert!(pair.read_server("a.txt").is_none());
}

#[tokio::test]
async fn a_folder_synced_by_an_older_version_keeps_its_history() {
let pair = Pair::new("legacy-state");
pair.write_local("a.txt", b"agreed");
pair.engine().sync_once().await.expect("first sync");
fs::rename(
pair.local.join(STATE_FILE_NAME),
pair.local.join(LEGACY_STATE_FILE_NAME),
)
.expect("stands in for a folder an older version synced");

fs::remove_file(pair.local.join("a.txt")).expect("removes the local copy");
let report = pair.engine().sync_once().await.expect("second sync");

assert_eq!(
report.deleted_remotely, 1,
"the deletion is recognised, so the old state was carried over"
);
assert!(pair.local.join(STATE_FILE_NAME).exists());
assert!(!pair.local.join(LEGACY_STATE_FILE_NAME).exists());
}

#[tokio::test]
async fn the_current_state_file_wins_over_one_an_older_version_left() {
let pair = Pair::new("both-states");
pair.write_local("a.txt", b"agreed");
pair.engine().sync_once().await.expect("first sync");
fs::write(pair.local.join(LEGACY_STATE_FILE_NAME), b"{}").expect("writes a stale legacy state");

fs::remove_file(pair.local.join("a.txt")).expect("removes the local copy");
let report = pair.engine().sync_once().await.expect("second sync");

assert_eq!(report.deleted_remotely, 1);
}

#[tokio::test]
async fn a_file_deleted_on_the_server_is_deleted_locally() {
let pair = Pair::new("delete-local");
Expand Down
21 changes: 14 additions & 7 deletions crates/client/src/sync/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ use tokio::task::JoinHandle;

use super::debounce::Debounce;
use super::engine::{Engine, Report, SyncError};
use super::state::STATE_FILE_NAME;
use super::state::is_state_file;
use super::transport::Transport;

const STATUS_BUFFER: usize = 64;
const PARTIAL_EXTENSION: &str = "roxypart";
const STATE_TEMP_NAME: &str = ".roxycloud-sync.tmp";
const PARTIAL_EXTENSIONS: [&str; 2] = ["stashpart", "roxypart"];
const STATE_TEMP_NAMES: [&str; 2] = [".stashden-sync.tmp", ".roxycloud-sync.tmp"];

#[derive(Debug, thiserror::Error)]
pub enum WatchError {
Expand Down Expand Up @@ -155,8 +155,8 @@ fn interesting(event: &notify::Event) -> bool {
fn is_ours(path: &Path) -> bool {
let name = path.file_name().and_then(|name| name.to_str());
let extension = path.extension().and_then(|extension| extension.to_str());
matches!(name, Some(STATE_FILE_NAME | STATE_TEMP_NAME))
|| matches!(extension, Some(PARTIAL_EXTENSION))
name.is_some_and(|name| is_state_file(name) || STATE_TEMP_NAMES.contains(&name))
|| extension.is_some_and(|extension| PARTIAL_EXTENSIONS.contains(&extension))
}

fn describe(error: &SyncError) -> String {
Expand Down Expand Up @@ -185,13 +185,20 @@ mod tests {

#[test]
fn the_state_file_does_not_trigger_another_run() {
assert!(!interesting(&event("/folder/.stashden-sync.json")));
assert!(!interesting(&event("/folder/.stashden-sync.tmp")));
}

#[test]
fn the_state_file_an_older_version_left_does_not_trigger_a_run_either() {
assert!(!interesting(&event("/folder/.roxycloud-sync.json")));
assert!(!interesting(&event("/folder/.roxycloud-sync.tmp")));
assert!(!interesting(&event("/folder/photos/x.jpg.roxypart")));
}

#[test]
fn a_partial_download_does_not_trigger_another_run() {
assert!(!interesting(&event("/folder/photos/x.jpg.roxypart")));
assert!(!interesting(&event("/folder/photos/x.jpg.stashpart")));
}

#[test]
Expand All @@ -201,7 +208,7 @@ mod tests {

#[test]
fn an_event_touching_both_is_still_worth_a_run() {
let mut both = event("/folder/.roxycloud-sync.json");
let mut both = event("/folder/.stashden-sync.json");
both.paths.push(PathBuf::from("/folder/a.txt"));
assert!(interesting(&both));
}
Expand Down
25 changes: 23 additions & 2 deletions web/src/app/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,28 @@ export class RequestFailed extends Error {
}
}

const TOKEN_KEY = 'roxycloud.token';
const SERVER_KEY = 'roxycloud.server';
const TOKEN_KEY = 'stashden.token';
const SERVER_KEY = 'stashden.server';
const KEY_PREFIX = 'stashden.';
const LEGACY_KEY_PREFIX = 'roxycloud.';

function adoptLegacyKeys(): void {
try {
const legacyKeys = Array.from({ length: localStorage.length }, (_, index) =>
localStorage.key(index),
).filter((key): key is string => key?.startsWith(LEGACY_KEY_PREFIX) ?? false);
for (const legacy of legacyKeys) {
const current = KEY_PREFIX + legacy.slice(LEGACY_KEY_PREFIX.length);
const value = localStorage.getItem(legacy);
if (value !== null && localStorage.getItem(current) === null) {
localStorage.setItem(current, value);
}
localStorage.removeItem(legacy);
}
} catch {
return;
}
}

const isDesktop = () => typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;

Expand Down Expand Up @@ -436,6 +456,7 @@ export function encodePath(path: string): string {
}

export function resolvePlatform(baseUrl: string): Platform {
adoptLegacyKeys();
return isDesktop() ? desktopPlatform(baseUrl) : browserPlatform(baseUrl);
}

Expand Down
2 changes: 1 addition & 1 deletion web/src/app/sync/sync-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
type Syncing,
} from './syncing';

const FOLDER_KEY = 'roxycloud.sync-folder';
const FOLDER_KEY = 'stashden.sync-folder';

@Component({
selector: 'rx-sync-view',
Expand Down
Loading