Skip to content
Draft
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
30 changes: 28 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:keyboard_dismisser/keyboard_dismisser.dart';
import 'package:logger/logger.dart';
import 'package:mobile_app_privacy/mobile_app_privacy.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:window_size/window_size.dart';

Expand Down Expand Up @@ -96,8 +97,33 @@ void main(List<String> args) async {
}
WidgetsFlutterBinding.ensureInitialized();

if (Util.isDesktop && args.length == 2 && args.first == "-d") {
StackFileSystem.setDesktopOverrideDir(args.last);
final appImagePath = Platform.isLinux
? Platform.environment["APPIMAGE"]
: null;
final portableMarkerExists =
appImagePath != null &&
File("${path.absolute(appImagePath)}.portable").existsSync();

if (args.contains("--portable") && appImagePath == null) {
stderr.writeln("--portable is only supported when running an AppImage.");
exitCode = 64;
return;
}

final dataDirectoryOverride = StackFileSystem.desktopDataDirectoryOverride(
arguments: args,
appImagePath: appImagePath,
appDataDirectoryName: AppConfig.appDefaultDataDirName,
portableMarkerExists: portableMarkerExists,
);
if (dataDirectoryOverride != null && Util.isDesktop) {
if (dataDirectoryOverride.portable) {
StackFileSystem.setPortableDesktopDataDirectory(
dataDirectoryOverride.path,
);
} else {
StackFileSystem.setDesktopOverrideDir(dataDirectoryOverride.path);
}
}

final loadCoinlibFuture = loadCoinlib();
Expand Down
77 changes: 63 additions & 14 deletions lib/utilities/stack_file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,62 @@ import 'util.dart';
abstract class StackFileSystem {
static String? _overrideDesktopDirPath;
static bool _overrideDirSet = false;
static bool _isPortable = false;

static bool get isPortableMode => _isPortable;

static void setDesktopOverrideDir(String dirPath) {
_setDesktopDataDirectory(dirPath, portable: false);
}

static void setPortableDesktopDataDirectory(String dirPath) {
_setDesktopDataDirectory(dirPath, portable: true);
}

static void _setDesktopDataDirectory(
String dirPath, {
required bool portable,
}) {
if (_overrideDirSet) {
throw Exception(
"Attempted to change StackFileSystem._overrideDir unexpectedly",
);
}
_overrideDesktopDirPath = dirPath;
_overrideDirSet = true;
_isPortable = portable;
}

/// Resolves an explicit AppImage portable-data request.
///
/// The `-d` override remains authoritative. Portable mode is otherwise
/// enabled by `--portable` or an `<AppImage>.portable` marker. An existing
/// data directory alone is deliberately not treated as consent.
static ({String path, bool portable})? desktopDataDirectoryOverride({
required List<String> arguments,
required String? appImagePath,
required String appDataDirectoryName,
required bool portableMarkerExists,
}) {
if (arguments.length == 2 && arguments.first == "-d") {
return (path: arguments.last, portable: false);
}

if (!arguments.contains("--portable") && !portableMarkerExists) {
return null;
}

if (appImagePath == null || appImagePath.trim().isEmpty) {
return null;
}

return (
path: path.join(
path.dirname(path.absolute(appImagePath)),
".$appDataDirectoryName",
),
portable: true,
);
}

static bool get _createSubDirs =>
Expand Down Expand Up @@ -213,23 +261,24 @@ abstract class StackFileSystem {
}
}

final appDocsDir = await getApplicationDocumentsDirectory();
const logsDirName = "${AppConfig.prefix}_Logs";
final Directory logsDir;

if (Platform.isIOS) {
logsDir = Directory(path.join(appDocsDir.path, "logs"));
} else if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) {
// TODO check this is correct for macos
logsDir = Directory(path.join(appDocsDir.path, logsDirName));
} else if (Platform.isAndroid) {
// final dir = await wtfAndroidDocumentsPath();
// final logsDirPath = path.join(dir.path, logsDirName);
// logsDir = Directory(logsDirPath);

logsDir = Directory(path.join(appDocsDir.path, "logs"));
if (_isPortable && _overrideDesktopDirPath != null) {
logsDir = Directory(path.join(_overrideDesktopDirPath!, "logs"));
} else {
throw Exception("Unsupported Platform");
final appDocsDir = await getApplicationDocumentsDirectory();
const logsDirName = "${AppConfig.prefix}_Logs";

if (Platform.isIOS) {
logsDir = Directory(path.join(appDocsDir.path, "logs"));
} else if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) {
// TODO check this is correct for macos
logsDir = Directory(path.join(appDocsDir.path, logsDirName));
} else if (Platform.isAndroid) {
logsDir = Directory(path.join(appDocsDir.path, "logs"));
} else {
throw Exception("Unsupported Platform");
}
}

if (!logsDir.existsSync()) {
Expand Down
88 changes: 88 additions & 0 deletions test/utilities/stack_file_system_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as path;
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/stack_file_system.dart';

void main() {
const appImagePath = "/media/secure/StackWallet.AppImage";

group("AppImage portable data directory", () {
test("is opt in", () {
final result = StackFileSystem.desktopDataDirectoryOverride(
arguments: const [],
appImagePath: appImagePath,
appDataDirectoryName: "stackwallet",
portableMarkerExists: false,
);

expect(result, isNull);
});

test("supports the portable flag", () {
final result = StackFileSystem.desktopDataDirectoryOverride(
arguments: const ["--portable"],
appImagePath: appImagePath,
appDataDirectoryName: "stackwallet",
portableMarkerExists: false,
);

expect(result, (path: "/media/secure/.stackwallet", portable: true));
});

test("supports a marker beside the AppImage", () {
final result = StackFileSystem.desktopDataDirectoryOverride(
arguments: const [],
appImagePath: appImagePath,
appDataDirectoryName: "stackwallet",
portableMarkerExists: true,
);

expect(result, (path: "/media/secure/.stackwallet", portable: true));
});

test("keeps the explicit data directory authoritative", () {
final result = StackFileSystem.desktopDataDirectoryOverride(
arguments: const ["-d", "/custom/data"],
appImagePath: appImagePath,
appDataDirectoryName: "stackwallet",
portableMarkerExists: true,
);

expect(result, (path: "/custom/data", portable: false));
});

test("does not enable portable mode outside an AppImage", () {
final result = StackFileSystem.desktopDataDirectoryOverride(
arguments: const ["--portable"],
appImagePath: null,
appDataDirectoryName: "stackwallet",
portableMarkerExists: false,
);

expect(result, isNull);
});
});

test("creates data and log directories inside the portable root", () async {
final temporaryDirectory = Directory.systemTemp.createTempSync(
"stack-wallet-portable-test-",
);
addTearDown(() => temporaryDirectory.deleteSync(recursive: true));
final portableRoot = path.join(temporaryDirectory.path, ".stackwallet");

StackFileSystem.setPortableDesktopDataDirectory(portableRoot);

final dataDirectory = await StackFileSystem.applicationRootDirectory();
final logsDirectory = await StackFileSystem.applicationLogsDirectory(
Prefs.instance,
);

expect(StackFileSystem.isPortableMode, isTrue);
expect(dataDirectory.path, portableRoot);
expect(dataDirectory.existsSync(), isTrue);
expect(logsDirectory.path, path.join(portableRoot, "logs"));
expect(logsDirectory.existsSync(), isTrue);
});
}
Loading