Restructure step R: the app is Starsync (dev.foxyz.starsync)
ci / core (push) Successful in 31s
ci / server (push) Successful in 1m3s
ci / updater-windows (push) Successful in 6m6s

- 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:
foxyzandClaude Opus 5.5 committed 2026-10-15 20:21:20 +03:00
1 parent f4ca02c3cf
commit 51c1c5ab78
96 files changed
+1169 -489

No files matched your search

+144 -22
View File
@@ -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);
});
});
}