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
26 changes: 12 additions & 14 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ Usage: discloud snapshot <subcommand> [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
Expand All @@ -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=<all> (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
Expand All @@ -436,7 +434,7 @@ Usage: discloud snapshot <subcommand> [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
Expand Down
4 changes: 2 additions & 2 deletions lib/commands/snapshot.dart
Original file line number Diff line number Diff line change
@@ -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<void> {
SnapshotCommand() {
addSubcommand(SnapshotCreateCommand());
addSubcommand(SnapshotDownloadCommand());
addSubcommand(SnapshotListCommand());
addSubcommand(SnapshotInfoCommand());
}

@override
Expand Down
8 changes: 7 additions & 1 deletion lib/commands/snapshot/create.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ final class SnapshotCreateCommand extends Command<void> {

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"]};
}
}
67 changes: 44 additions & 23 deletions lib/commands/snapshot/download.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,21 @@ 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";
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<void> 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: ".",
);
}

Expand All @@ -40,53 +30,84 @@ final class SnapshotDownloadCommand extends Command<void> with Disposable {
@override
final aliases = const ["dl"];

HttpClient? _client;
File? _file;
HttpClient? _client;
SpeedMonitor? _monitor;

@override
Future<void> 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",
);

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<String> _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 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 snapshot) {
if (snapshot["version"] case final String value) return value;
}
}

final Uri uri = Uri.parse(url);
throw Exception("Failed to retrieve an app snapshot version");
}

Future<void> _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...");

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);
Expand Down
122 changes: 122 additions & 0 deletions lib/commands/snapshot/info.dart
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> run() {
return switch (argResults?.option("app")) {
final String appId => _runSingle(appId),
_ => _runMulti(),
};
}

Future<void> _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<void> _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<Map<String, dynamic>> _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<Map<String, dynamic>> _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();
}
}
Loading
Loading