From 08f06b5b4a7ab0f2843214d26242bb8fd4063986 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Sun, 5 Jul 2026 15:43:06 -0300 Subject: [PATCH 1/3] feat(snapshot): replace list command with info command and enhance snapshot creation --- lib/commands/snapshot.dart | 4 +- lib/commands/snapshot/create.dart | 8 +- lib/commands/snapshot/download.dart | 63 ++++++++------ lib/commands/snapshot/info.dart | 122 ++++++++++++++++++++++++++++ lib/commands/snapshot/list.dart | 77 ------------------ 5 files changed, 171 insertions(+), 103 deletions(-) create mode 100644 lib/commands/snapshot/info.dart delete mode 100644 lib/commands/snapshot/list.dart diff --git a/lib/commands/snapshot.dart b/lib/commands/snapshot.dart index e7542b8..345bf3e 100644 --- a/lib/commands/snapshot.dart +++ b/lib/commands/snapshot.dart @@ -1,13 +1,13 @@ import "package:args/command_runner.dart"; import "package:discloud/commands/snapshot/create.dart"; import "package:discloud/commands/snapshot/download.dart"; -import "package:discloud/commands/snapshot/list.dart"; +import "package:discloud/commands/snapshot/info.dart"; final class SnapshotCommand extends Command { SnapshotCommand() { addSubcommand(SnapshotCreateCommand()); addSubcommand(SnapshotDownloadCommand()); - addSubcommand(SnapshotListCommand()); + addSubcommand(SnapshotInfoCommand()); } @override diff --git a/lib/commands/snapshot/create.dart b/lib/commands/snapshot/create.dart index d870179..e1e7944 100644 --- a/lib/commands/snapshot/create.dart +++ b/lib/commands/snapshot/create.dart @@ -27,6 +27,12 @@ final class SnapshotCreateCommand extends Command { spinner.success(resolveResponseMessage(response)); - stdout.writeln(mapToVerticalAsciiTable(response["snapshot"])); + if (response["snapshot"] case final Map snapshot) { + stdout.writeln(mapToVerticalAsciiTable(_flattenSnapshot(snapshot))); + } + } + + Map _flattenSnapshot(Map snapshot) { + return {"Version": snapshot["version"], "Size": snapshot["version"]}; } } diff --git a/lib/commands/snapshot/download.dart b/lib/commands/snapshot/download.dart index 9a88341..188601f 100644 --- a/lib/commands/snapshot/download.dart +++ b/lib/commands/snapshot/download.dart @@ -3,6 +3,7 @@ import "dart:io"; import "package:args/command_runner.dart"; import "package:discloud/cli/disposable.dart"; +import "package:discloud/cli/spin/ispin.dart"; import "package:discloud/extensions/command.dart"; import "package:discloud/extensions/file.dart"; import "package:discloud/utils/download.dart"; @@ -10,24 +11,13 @@ import "package:discloud/utils/messages.dart"; import "package:discloud/utils/progress.dart"; import "package:discloud/utils/speed_monitor.dart"; -const _pSep = "/"; - final class SnapshotDownloadCommand extends Command with Disposable { SnapshotDownloadCommand() { argParser ..addOption("app", mandatory: true) ..addOption( "version", - mandatory: true, help: "Snapshot version in YYYYMMDD-HHMMSS format", - ) - ..addOption( - "dir", - abbr: "d", - aliases: const ["out"], - help: - "Specifies the destination path for downloading the snapshot. The destination path will be considered a directory.", - defaultsTo: ".", ); } @@ -40,17 +30,19 @@ final class SnapshotDownloadCommand extends Command with Disposable { @override final aliases = const ["dl"]; - HttpClient? _client; File? _file; + HttpClient? _client; SpeedMonitor? _monitor; @override Future run() async { final String appId = argResults!.option("app")!; - final String version = argResults!.option("version")!; - final String dir = argResults!.option("dir")!; - final spinner = context.printer.spin(text: "Fetching download url..."); + final spinner = context.printer.spin(); + + final version = await _retrieveSnapshotVersion(appId, spinner: spinner); + + spinner.text = "Fetching snapshot download url..."; final response = await context.api.get( "/snapshot/$appId/versions/$version", @@ -58,16 +50,40 @@ final class SnapshotDownloadCommand extends Command with Disposable { spinner.success(resolveResponseMessage(response)); - final String url = response["download"]["url"]; + if (response["download"]?["url"] case final String url) { + await _download(spinner: spinner, uri: .parse(url)); + } + } + + Future _retrieveSnapshotVersion( + String appId, { + required ISpin spinner, + }) async { + if (argResults?.option("version") case final String v) return v; + + spinner.text = "Retrieving snapshot..."; + + if (await context.api.get("/snapshot/$appId") case final Map r) { + if (r["versions"] case final List l when l.isNotEmpty) { + if (l.first case final Map v) return v["version"]; + } + } + + spinner.text = "Creating snapshot..."; + + if (await context.api.post("/snapshot/$appId") case final Map r) { + if (r["snapshot"] case final Map s) return s["version"]; + } - final Uri uri = Uri.parse(url); + throw Exception("Failed to retrieve an app snapshot version"); + } + Future _download({required ISpin spinner, required Uri uri}) async { final filename = uri.pathSegments.last; - final filepath = "$dir$_pSep$filename"; - final file = _file = File(filepath); + final filepath = filename; + final file = _file = .new(filepath); - final client = _client = HttpClient(); - final monitor = _monitor = SpeedMonitor(); + final monitor = _monitor = .new(); try { spinner.start("Downloading..."); @@ -75,18 +91,19 @@ final class SnapshotDownloadCommand extends Command with Disposable { await download( uri, file: file, - client: client, + client: _client, onProgress: (processed, total) { spinner.text = formatProgressMessage( prefixText: "Downloading:", speed: monitor.add(processed), - direction: UnitDirection.down, + direction: .down, processed: processed, total: total, ); }, ); + // no delete on dispose _file = null; spinner.success(filepath); diff --git a/lib/commands/snapshot/info.dart b/lib/commands/snapshot/info.dart new file mode 100644 index 0000000..1b54ef2 --- /dev/null +++ b/lib/commands/snapshot/info.dart @@ -0,0 +1,122 @@ +import "dart:async"; +import "dart:io"; + +import "package:args/command_runner.dart"; +import "package:discloud/extensions/command.dart"; +import "package:discloud/utils/ascii_table.dart"; +import "package:discloud/utils/messages.dart"; + +final class SnapshotInfoCommand extends Command { + SnapshotInfoCommand() { + argParser + ..addOption("app") + ..addOption( + "page", + abbr: "p", + help: "Page number (must be >= 1)", + valueHelp: "1", + ) + ..addOption( + "limit", + abbr: "l", + help: "Items per page (1–200)", + valueHelp: "50", + ) + ..addFlag( + "summary", + abbr: "s", + help: "shows a summary view of each backup", + negatable: false, + ); + } + + @override + final name = "info"; + + @override + final description = "List your versioned snapshots"; + + @override + final aliases = const ["ls"]; + + @override + Future run() { + return switch (argResults?.option("app")) { + final String appId => _runSingle(appId), + _ => _runMulti(), + }; + } + + Future _runSingle(String appId) async { + final spinner = context.printer.spin(text: "Fetching snapshots..."); + + final response = await context.api.get("/snapshot/$appId"); + + spinner.success(resolveResponseMessage(response)); + + if (response["versions"] case final List list) { + stdout.writeln(listToAsciiTable(_flattenSnapshotVersion(list))); + } + } + + Future _runMulti() async { + final spinner = context.printer.spin(text: "Fetching snapshots..."); + + final response = await context.api.get( + "/snapshot", + query: { + "page": ?argResults?.option("page"), + "limit": ?argResults?.option("limit"), + "summary": ?argResults?.flag("summary").toString(), + }, + ); + + spinner.success(resolveResponseMessage(response)); + + if (response["apps"] case final List list) { + stdout.writeln(listToAsciiTable(_flattenSnapshotApp(list))); + if (response["pagination"] case final Map pagination) { + stdout.writeln(_showSnapshotPagination(pagination)); + } + } + } + + List> _flattenSnapshotApp(List list) { + return [ + for (final map in list) + if (map case final Map map) + { + "App ID": map["appID"], + "Versions": ?map["versionsCount"], + "Total size": ?map["totalSize"], + if (map["latestBackup"] case final Map lastBackup) + "Last backup": StringBuffer() + ..writeAll([lastBackup["version"], lastBackup["size"]], " "), + }, + ]; + } + + List> _flattenSnapshotVersion(List list) { + return [ + for (final map in list) + if (map case final Map map) + { + "Version": map["version"], + "Size": map["size"], + "Last modified": map["lastModified"], + }, + ]; + } + + String? _showSnapshotPagination(Map pagination) { + final StringBuffer buffer = .new() + ..writeAll([ + if (pagination["currentPage"] case final int page) ...[ + "Page $page", + if (pagination["totalPages"] case final int total) " of $total", + ] else if (pagination["totalPages"] case final int total) + "Pages: $total", + ]); + return buffer.toString(); + } +} diff --git a/lib/commands/snapshot/list.dart b/lib/commands/snapshot/list.dart deleted file mode 100644 index eef3acd..0000000 --- a/lib/commands/snapshot/list.dart +++ /dev/null @@ -1,77 +0,0 @@ -import "dart:async"; -import "dart:io"; - -import "package:args/command_runner.dart"; -import "package:discloud/extensions/command.dart"; -import "package:discloud/utils/ascii_table.dart"; -import "package:discloud/utils/messages.dart"; - -final class SnapshotListCommand extends Command { - SnapshotListCommand() { - argParser - ..addOption("app", defaultsTo: "all", valueHelp: "all") - ..addOption("page", defaultsTo: "1") - ..addOption("limit", defaultsTo: "50") - ..addFlag("summary", negatable: false); - } - - @override - final name = "list"; - - @override - final description = "List your versioned snapshots"; - - @override - final aliases = const ["ls"]; - - @override - Future run() async { - final String appId = argResults!.option("app")!; - - final spinner = context.printer.spin(text: "Fetching snapshots..."); - - if (appId == "all") { - final response = await context.api.get( - "/snapshot", - query: { - "page": argResults!.option("page")!, - "limit": argResults!.option("limit")!, - "summary": "${argResults!.flag("summary")}", - }, - ); - - spinner.success(resolveResponseMessage(response)); - - switch (response["apps"]) { - case final List list: - stdout.writeln(listToAsciiTable(_flattenApps(list))); - break; - } - - return; - } - - final response = await context.api.get("/snapshot/$appId"); - - spinner.success(resolveResponseMessage(response)); - - switch (response["versions"]) { - case final List list: - stdout.writeln(listToAsciiTable(list)); - break; - } - } - - List> _flattenApps(List list) { - return [ - for (final app in list) - { - "appID": app["appID"], - "versionsCount": app["versionsCount"], - "totalSize": app["totalSize"], - "latestVersion": app["latestBackup"]?["version"], - "lastModified": app["latestBackup"]?["lastModified"], - }, - ]; - } -} From 74a1134ff34c700424d093c576a5d7abaa6402f9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 5 Jul 2026 18:44:16 +0000 Subject: [PATCH 2/3] docs: update 08f06b5 --- docs/commands.md | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 5c66aef..44894d5 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -386,7 +386,7 @@ Usage: discloud snapshot [arguments] Available subcommands: create Create a versioned snapshot of your app download Download a versioned snapshot of your app - list List your versioned snapshots + info List your versioned snapshots ``` #### snapshot create @@ -405,24 +405,22 @@ Usage: discloud snapshot create [arguments] Download a versioned snapshot of your app Usage: discloud snapshot download [arguments] --h, --help Print this usage information. - --app (mandatory) - --version (mandatory) Snapshot version in YYYYMMDD-HHMMSS format --d, --dir Specifies the destination path for downloading the snapshot. The destination path will be considered a directory. - (defaults to ".") +-h, --help Print this usage information. + --app (mandatory) + --version Snapshot version in YYYYMMDD-HHMMSS format ``` -#### snapshot list +#### snapshot info ```sh List your versioned snapshots -Usage: discloud snapshot list [arguments] --h, --help Print this usage information. - --app= (defaults to "all") - --page (defaults to "1") - --limit (defaults to "50") - --summary +Usage: discloud snapshot info [arguments] +-h, --help Print this usage information. + --app +-p, --page=<1> Page number (must be >= 1) +-l, --limit=<50> Items per page (1–200) +-s, --summary shows a summary view of each backup ``` ### snapshot @@ -436,7 +434,7 @@ Usage: discloud snapshot [arguments] Available subcommands: create Create a versioned snapshot of your app download Download a versioned snapshot of your app - list List your versioned snapshots + info List your versioned snapshots ``` ### subdomain From f98d6f5e8cac4eb0e9e3c3181fe2d35ba2524c5f Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Sun, 5 Jul 2026 16:31:05 -0300 Subject: [PATCH 3/3] fix(snapshot): improve version retrieval logic in snapshot download command --- lib/commands/snapshot/download.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/commands/snapshot/download.dart b/lib/commands/snapshot/download.dart index 188601f..400bde8 100644 --- a/lib/commands/snapshot/download.dart +++ b/lib/commands/snapshot/download.dart @@ -64,15 +64,19 @@ final class SnapshotDownloadCommand extends Command with Disposable { spinner.text = "Retrieving snapshot..."; if (await context.api.get("/snapshot/$appId") case final Map r) { - if (r["versions"] case final List l when l.isNotEmpty) { - if (l.first case final Map v) return v["version"]; + if (r["versions"] case final List versions when versions.isNotEmpty) { + if (versions.first case final Map version) { + if (version["version"] case final String value) return value; + } } } spinner.text = "Creating snapshot..."; if (await context.api.post("/snapshot/$appId") case final Map r) { - if (r["snapshot"] case final Map s) return s["version"]; + if (r["snapshot"] case final Map snapshot) { + if (snapshot["version"] case final String value) return value; + } } throw Exception("Failed to retrieve an app snapshot version");