Restructure step R: the app is Starsync (dev.foxyz.starsync)
- Linux app id dev.foxyz.starsync, binary starsync; Windows starsync.exe, company foxyz; server starsync-server; Dart package starsync; archives starsync-<ver>-*. - 1.2.x compatibility: watch_together and watch-together-server launchers (Linux) / copies (Windows) in the archive; the updater accepts both names and relaunches the new binary; update.sh/.bat use whichever server exists. - First start copies (never moves) the old support folder and this app's Hive boxes out of ~/Documents into <support>/hive; a marker makes it run once; AppDirs is a port; settings schema version with ordered migrations. - Launcher entry dev.foxyz.starsync.desktop; the old one removed. - Repository name and URLs unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f4ca02c3cf
commit
51c1c5ab78
96 files changed
+1169
-489
No files matched your search
@@ -1,36 +1,158 @@
|
||||
// restructure: rewrite in step R (Rename): Hive moves into the support
|
||||
// folder for the normal app too.
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:watch_together/src/platform/app_dirs.dart';
|
||||
import 'package:starsync/src/files/folder_store.dart' show filesBoxName;
|
||||
import 'package:starsync/src/jellyfin/accounts.dart' show jellyfinBoxName;
|
||||
import 'package:starsync/src/jellyfin/track_prefs.dart' show trackPrefsBoxName;
|
||||
import 'package:starsync/src/platform/data_migration.dart';
|
||||
import 'package:starsync/src/platform/ports/app_dirs.dart';
|
||||
import 'package:starsync/src/state/connect_prefs.dart' show connectFormBoxName;
|
||||
import 'package:starsync/src/state/settings.dart' show settingsBoxName;
|
||||
|
||||
void main() {
|
||||
final sep = Platform.pathSeparator;
|
||||
const support = '/s';
|
||||
|
||||
test('no profile: today\'s layout', () {
|
||||
final d = AppDirs.fromEnv(support: support, env: const {});
|
||||
expect(d.profile, isNull);
|
||||
expect(d.hive, isNull, reason: "Hive's default folder");
|
||||
expect(d.logs, '$support${sep}logs');
|
||||
group('AppDirs', () {
|
||||
test('no profile: Hive and logs in the support folder', () {
|
||||
final d = AppDirs.fromEnv(support: support, env: const {});
|
||||
expect(d.profile, isNull);
|
||||
expect(d.hive, '$support${sep}hive');
|
||||
expect(d.logs, '$support${sep}logs');
|
||||
});
|
||||
|
||||
test('a profile gets its own Hive and logs folders', () {
|
||||
final d = AppDirs.fromEnv(support: support, env: const {profileEnv: 'b'});
|
||||
expect(d.profile, 'b');
|
||||
expect(d.hive, '$support${sep}profiles${sep}b${sep}hive');
|
||||
expect(d.logs, '$support${sep}profiles${sep}b${sep}logs');
|
||||
expect(d.support, support, reason: 'yt-dlp and shaders stay shared');
|
||||
});
|
||||
|
||||
test('an unusable name is ignored and reported', () {
|
||||
for (final bad in ['../x', 'a b', 'x' * 33, 'é']) {
|
||||
final env = {profileEnv: bad};
|
||||
expect(AppDirs.fromEnv(support: support, env: env).profile, isNull, reason: bad);
|
||||
expect(AppDirs.invalidProfile(env), bad);
|
||||
}
|
||||
expect(AppDirs.invalidProfile(const {profileEnv: ''}), isNull, reason: 'empty is just unset');
|
||||
expect(AppDirs.invalidProfile(const {profileEnv: 'ok_1-2'}), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
test('a profile gets its own Hive and logs folders', () {
|
||||
final d = AppDirs.fromEnv(support: support, env: const {profileEnv: 'b'});
|
||||
expect(d.profile, 'b');
|
||||
expect(d.hive, '$support${sep}profiles${sep}b${sep}hive');
|
||||
expect(d.logs, '$support${sep}profiles${sep}b${sep}logs');
|
||||
expect(d.support, support, reason: 'yt-dlp and shaders stay shared');
|
||||
test('the migrated boxes are the ones the app opens', () {
|
||||
expect(appHiveBoxes.toSet(),
|
||||
{settingsBoxName, connectFormBoxName, filesBoxName, jellyfinBoxName, trackPrefsBoxName});
|
||||
});
|
||||
|
||||
test('an unusable name is ignored and reported', () {
|
||||
for (final bad in ['../x', 'a b', 'x' * 33, 'é']) {
|
||||
final env = {profileEnv: bad};
|
||||
expect(AppDirs.fromEnv(support: support, env: env).profile, isNull, reason: bad);
|
||||
expect(AppDirs.invalidProfile(env), bad);
|
||||
}
|
||||
expect(AppDirs.invalidProfile(const {profileEnv: ''}), isNull, reason: 'empty is just unset');
|
||||
expect(AppDirs.invalidProfile(const {profileEnv: 'ok_1-2'}), isNull);
|
||||
test('old support folders next to the new one', () {
|
||||
expect(legacySupportDirs('/home/u/.local/share/dev.foxyz.starsync', windows: false), [
|
||||
'/home/u/.local/share/moe.senkochan.watch_together',
|
||||
'/home/u/.local/share/watch_together',
|
||||
]);
|
||||
expect(legacySupportDirs(r'C:\Users\u\AppData\Roaming\foxyz\Starsync', windows: true),
|
||||
[r'C:\Users\u\AppData\Roaming\com.example\watch_together']);
|
||||
});
|
||||
|
||||
group('migration from before the rename', () {
|
||||
late Directory home;
|
||||
late String share, oldSupport, support, docs, hive;
|
||||
|
||||
setUp(() async {
|
||||
home = await Directory.systemTemp.createTemp('starsync-mig');
|
||||
share = '${home.path}/.local/share';
|
||||
oldSupport = '$share/moe.senkochan.watch_together';
|
||||
support = '$share/dev.foxyz.starsync';
|
||||
docs = '${home.path}/Documents';
|
||||
hive = '$support/hive';
|
||||
// The old support folder as 1.2.x left it.
|
||||
await Directory('$oldSupport/yt-dlp').create(recursive: true);
|
||||
await File('$oldSupport/yt-dlp/cookies.txt').writeAsString('# Netscape cookies');
|
||||
await File('$oldSupport/yt-dlp/yt-dlp').writeAsString('bin');
|
||||
await Directory('$oldSupport/logs').create();
|
||||
await File('$oldSupport/logs/app-2026-01-01.log').writeAsString('log');
|
||||
await Directory('$oldSupport/shaders').create();
|
||||
await File('$oldSupport/shaders/a.glsl').writeAsString('shader');
|
||||
await Directory('$oldSupport/profiles/b/hive').create(recursive: true);
|
||||
await File('$oldSupport/profiles/b/hive/settings.hive').writeAsString('b-settings');
|
||||
if (!Platform.isWindows) {
|
||||
await Process.run('chmod', ['600', '$oldSupport/yt-dlp/cookies.txt']);
|
||||
await Process.run('chmod', ['755', '$oldSupport/yt-dlp/yt-dlp']);
|
||||
await Link('$oldSupport/yt-dlp/current').create('yt-dlp');
|
||||
}
|
||||
// The documents folder: this app's boxes and another app's.
|
||||
await Directory(docs).create(recursive: true);
|
||||
for (final n in ['settings', 'jellyfin', 'files', 'track_prefs', 'connect_form', 'app', 'credentials']) {
|
||||
await File('$docs/$n.hive').writeAsString('$n-data');
|
||||
await File('$docs/$n.lock').writeAsString('');
|
||||
}
|
||||
// path_provider creates the new folder before the app looks.
|
||||
await Directory(support).create(recursive: true);
|
||||
});
|
||||
|
||||
tearDown(() => home.delete(recursive: true));
|
||||
|
||||
Future<List<String>> migrate() => migrateLegacyData(
|
||||
support: support, hive: hive, oldSupport: legacySupportDirs(support, windows: false), documents: docs);
|
||||
|
||||
test('copies the support folder and only this app\'s boxes, once', () async {
|
||||
final lines = await migrate();
|
||||
expect(lines, hasLength(2));
|
||||
expect(lines.first, contains(oldSupport));
|
||||
expect(lines.last, contains('5 Hive boxes'));
|
||||
expect(lines.join(), isNot(contains('Netscape')), reason: 'paths and counts, never contents');
|
||||
|
||||
expect(await File('$support/yt-dlp/cookies.txt').readAsString(), '# Netscape cookies');
|
||||
expect(await File('$support/logs/app-2026-01-01.log').readAsString(), 'log');
|
||||
expect(await File('$support/shaders/a.glsl').readAsString(), 'shader');
|
||||
expect(await File('$support/profiles/b/hive/settings.hive').readAsString(), 'b-settings',
|
||||
reason: 'profiles come along with the support folder');
|
||||
if (!Platform.isWindows) {
|
||||
expect(File('$support/yt-dlp/cookies.txt').statSync().mode & 0x1ff, 0x180, reason: '0600 kept');
|
||||
expect(File('$support/yt-dlp/yt-dlp').statSync().mode & 0x40, isNonZero, reason: 'exec bit kept');
|
||||
expect(await Link('$support/yt-dlp/current').target(), 'yt-dlp');
|
||||
}
|
||||
for (final n in appHiveBoxes) {
|
||||
expect(await File('$hive/$n.hive').readAsString(), '$n-data');
|
||||
expect(File('$hive/$n.lock').existsSync(), isTrue);
|
||||
}
|
||||
expect(File('$hive/app.hive').existsSync(), isFalse);
|
||||
expect(File('$hive/credentials.hive').existsSync(), isFalse);
|
||||
// Copied, not moved: 1.2.x keeps working.
|
||||
expect(File('$docs/settings.hive').existsSync(), isTrue);
|
||||
expect(File('$oldSupport/yt-dlp/cookies.txt').existsSync(), isTrue);
|
||||
|
||||
final marker = jsonDecode(File('$support/$migrationMarker').readAsStringSync()) as Map;
|
||||
expect(marker['migratedFrom'], oldSupport);
|
||||
expect(marker['hiveFrom'], docs);
|
||||
expect(marker['files'], greaterThan(0));
|
||||
|
||||
// Once: a later change on the old side is not copied again.
|
||||
await File('$docs/settings.hive').writeAsString('changed by 1.2.x');
|
||||
await File('$hive/settings.hive').delete();
|
||||
expect(await migrate(), isEmpty);
|
||||
expect(File('$hive/settings.hive').existsSync(), isFalse);
|
||||
});
|
||||
|
||||
test('never overwrites what the new folder already has', () async {
|
||||
await Directory(hive).create(recursive: true);
|
||||
await File('$hive/settings.hive').writeAsString('new');
|
||||
await Directory('$support/yt-dlp').create();
|
||||
await File('$support/yt-dlp/cookies.txt').writeAsString('new cookies');
|
||||
await migrate();
|
||||
expect(await File('$hive/settings.hive').readAsString(), 'new');
|
||||
expect(await File('$support/yt-dlp/cookies.txt').readAsString(), 'new cookies');
|
||||
expect(await File('$hive/jellyfin.hive').readAsString(), 'jellyfin-data');
|
||||
});
|
||||
|
||||
test('a fresh install finds nothing and still marks it done', () async {
|
||||
await Directory(oldSupport).delete(recursive: true);
|
||||
await Directory(docs).delete(recursive: true);
|
||||
expect(await migrate(), ['migration: no data from before the rename']);
|
||||
final marker = jsonDecode(File('$support/$migrationMarker').readAsStringSync()) as Map;
|
||||
expect(marker['migratedFrom'], isNull);
|
||||
expect(marker['hiveFrom'], isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user