Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix extracting docker images #249

Merged
merged 4 commits into from
May 16, 2024
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
Binary file added 7zip/7za.dll
Binary file not shown.
Binary file added 7zip/7za.exe
Binary file not shown.
Binary file added 7zip/7zxa.dll
Binary file not shown.
68 changes: 68 additions & 0 deletions lib/api/archive.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import 'dart:io';

import 'package:archive/archive_io.dart';

/// API for archive operations
class ArchiveApi {
/// Extracts the archive at [archivePath] to [destinationPath]
static Future<void> extract(
String archivePath, String destinationPath) async {
// Extract archive
final inputStream = InputFileStream(archivePath);
final extracted = GZipDecoder().decodeBuffer(inputStream);
// Write to destination
final outputFile = File(destinationPath);
outputFile.create(recursive: true);
await outputFile.writeAsBytes(extracted);
inputStream.close();
}

/// Merge the tar archives at [archivePaths] into [destinationPath]
/// Trailing zeros are removed from the files.
static Future<void> merge(
List<String> archivePaths, String destinationPath) async {
// Merge archives
try {
// remove trailing zeros from the files
final outputFile = File(destinationPath);
for (var i = 0; i < archivePaths.length; i++) {
final fileName = archivePaths[i];
// Read file as byte stream
final file = File(fileName);
final bytes = await file.readAsBytes();
final length = bytes.length;

// Last layer
if (i == archivePaths.length - 1) {
await outputFile.writeAsBytes(bytes);
break;
}

// Remove trailing zeros
int lastBytePos = 0;
for (var i = length - 1; i >= 0; i--) {
if (bytes[i] != 0) {
lastBytePos = i;
break;
}
}

// Write to new file
await outputFile.writeAsBytes(bytes.sublist(0, lastBytePos + 1));
}
} catch (e) {
throw Exception('Failed to merge archives: $e');
}
}

/// Compress the tar archive at [filePath] to [destinationPath]
static void compress(String filePath, String destinationPath) {
// compress tar to gzip
final inputFileStream = InputFileStream(filePath);
final outputFileStream = OutputFileStream(destinationPath);
GZipEncoder().encode(inputFileStream,
output: outputFileStream, level: Deflate.BEST_SPEED);
inputFileStream.close();
outputFileStream.close();
}
}
40 changes: 14 additions & 26 deletions lib/api/docker_images.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@

import 'dart:convert';
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:archive/archive_io.dart';
import 'package:chunked_downloader/chunked_downloader.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:localization/localization.dart';
import 'package:wsl2distromanager/api/archive.dart';
import 'package:wsl2distromanager/api/safe_paths.dart';
import 'package:wsl2distromanager/components/helpers.dart';
import 'package:wsl2distromanager/components/logging.dart';
Expand Down Expand Up @@ -433,12 +432,13 @@ class DockerImage {
// Write the compressed tar file to disk.
int retry = 0;

String outArchive = SafePath(distroPath).file('$imageName.tar.gz');
final parentPath = SafePath(tmpImagePath);
String outTar = parentPath.file('$imageName.tar');
String outTarGz = SafePath(distroPath).file('$imageName.tar.gz');
while (retry < 2) {
try {
Archive archive = Archive();

// More than one layer
List<String> paths = [];
if (layers != 1) {
for (var i = 0; i < layers; i++) {
// Read archives layers
Expand All @@ -448,34 +448,22 @@ class DockerImage {
// progress(i, layers, -1, -1);
Notify.message('Extracting layer $i of $layers');

// In memory
final tarfile = GZipDecoder().decodeBytes(
File(SafePath(tmpImagePath).file('layer_$i.tar.gz'))
.readAsBytesSync());
final subArchive = TarDecoder().decodeBytes(tarfile);

// Add files to archive
for (final file in subArchive) {
archive.addFile(file);
if (kDebugMode && !file.name.contains('/')) {
if (kDebugMode) {
print('Adding root file ${file.name}');
}
}
}
// Extract layer
final layerTarGz = parentPath.file('layer_$i.tar.gz');
final layerTar = parentPath.file('layer_$i.tar');
await ArchiveApi.extract(layerTarGz, layerTar);
paths.add(layerTar);
}

// Archive as tar then gzip to disk
final tarfile = TarEncoder().encode(archive);
final gzData = GZipEncoder().encode(tarfile);
final fp = File(outArchive);
await ArchiveApi.merge(paths, outTar);
ArchiveApi.compress(outTar, outTarGz);

Notify.message('writingtodisk-text'.i18n());
fp.writeAsBytesSync(gzData!);
} else if (layers == 1) {
// Just copy the file
File(SafePath(tmpImagePath).file('layer_0.tar.gz'))
.copySync(outArchive);
.copySync(outTarGz);
}

retry = 2;
Expand All @@ -495,7 +483,7 @@ class DockerImage {
Notify.message('creatinginstance-text'.i18n());

// Check if tar file is created
if (!File(outArchive).existsSync()) {
if (!File(outTarGz).existsSync()) {
throw Exception('Tar file is not created');
}
// Wait for tar file to be created
Expand Down
33 changes: 18 additions & 15 deletions lib/dialogs/create_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ createDialog() {
);
}

progressFn(current, total, currentStep, totalStep) {
if (currentStep != -1) {
String progressInMB = (currentStep / 1024 / 1024).toStringAsFixed(2);
// String totalInMB = (total / 1024 / 1024).toStringAsFixed(2);
String percentage = (currentStep / totalStep * 100).toStringAsFixed(0);
Notify.message('${'downloading-text'.i18n()}'
' Layer ${current + 1}/$total: $percentage% ($progressInMB MB)');
} else {
Notify.message('extractinglayers-text'.i18n(['$current', '$total']));
}
}

Future<void> createInstance(
TextEditingController nameController,
TextEditingController locationController,
Expand Down Expand Up @@ -117,21 +129,12 @@ Future<void> createInstance(
// Download image
Notify.message('${'downloading-text'.i18n()}...');
var docker = DockerImage()..distroName = distroName;
await docker.getRootfs(name, image, tag: tag,
progress: (current, total, currentStep, totalStep) {
if (currentStep != -1) {
String progressInMB =
(currentStep / 1024 / 1024).toStringAsFixed(2);
// String totalInMB = (total / 1024 / 1024).toStringAsFixed(2);
String percentage =
(currentStep / totalStep * 100).toStringAsFixed(0);
Notify.message('${'downloading-text'.i18n()}'
' Layer ${current + 1}/$total: $percentage% ($progressInMB MB)');
} else {
Notify.message(
'extractinglayers-text'.i18n(['$current', '$total']));
}
});
try {
await docker.getRootfs(name, image, tag: tag, progress: progressFn);
} catch (e) {
Notify.message('error-text'.i18n());
return;
}
Notify.message('downloaded-text'.i18n());
// Set distropath with distroName
distroName = DockerImage().filename(image, tag);
Expand Down
4 changes: 2 additions & 2 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ packages:
dependency: "direct main"
description:
name: archive
sha256: "0763b45fa9294197a2885c8567927e2830ade852e5c896fd4ab7e0e348d0f373"
sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265
url: "https://pub.dev"
source: hosted
version: "3.5.0"
version: "3.5.1"
args:
dependency: transitive
description:
Expand Down
6 changes: 3 additions & 3 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: wsl2distromanager
description: A GUI to quickly manage your WSL instances.

publish_to: "none"
version: 1.8.13 # Current version
version: 1.8.14 # Current version

environment:
sdk: ">=2.17.0 <4.0.0"
Expand All @@ -12,7 +12,7 @@ dependencies:
sdk: flutter
flutter_localizations:
sdk: flutter
archive: ^3.3.7
archive: ^3.5.1
async: ^2.11.0
chunked_downloader: ^0.0.2
desktop_window: ^0.4.0
Expand Down Expand Up @@ -67,4 +67,4 @@ msix_config:
architecture: x64
capabilities: "internetClient"
store: true
languages: en-US, en-GB, en-AU, de-DE, pt-BR, pt-PT, zh-CN, zh-TW
languages: en-US, en-GB, en-AU, de-DE, pt-BR, pt-PT, zh-CN, zh-TW, tr-TR
54 changes: 52 additions & 2 deletions test/dockerimages_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ void main() {

// Delete the instance
await WSLApi().remove('test');
});
}, timeout: const Timeout(Duration(minutes: 2)));

test('Create instance test nginx (nonroot)', () async {
TextEditingController nameController = TextEditingController(text: 'test');
Expand Down Expand Up @@ -192,5 +192,55 @@ void main() {
// Verify that the file exists and has > 2MB
expect(await file.exists(), true);
expect(await file.length(), greaterThan(2 * 1024 * 1024));
}, timeout: const Timeout(Duration(minutes: 2)));
}, timeout: const Timeout(Duration(minutes: 10)));

// Test almalinux latest
test('Create instance test almalinux:latest', () async {
TextEditingController nameController = TextEditingController(text: 'test');
TextEditingController locationController = TextEditingController(text: '');
TextEditingController autoSuggestBox =
TextEditingController(text: 'dockerhub:almalinux:latest');

final file =
File('C:/WSL2-Distros/distros/library_almalinux_latest.tar.gz');
if (await file.exists()) {
await file.delete();
}

// Delete the instance
await WSLApi().remove('test');

// Test build context
await createInstance(
nameController,
locationController,
WSLApi(),
autoSuggestBox,
TextEditingController(text: ''),
);

// Verify that the file exists and has > 2MB
expect(await file.exists(), true);
expect(await file.length(), greaterThan(2 * 1024 * 1024));
expect(await isInstance('test'), true);

// Delete the instance
await WSLApi().remove('test');

expect(await isInstance('test'), false);

// Test creating it without re-downloading the rootfs
await createInstance(
nameController,
locationController,
WSLApi(),
autoSuggestBox,
TextEditingController(text: ''),
);

expect(await isInstance('test'), true);

// Delete the instance
await WSLApi().remove('test');
}, timeout: const Timeout(Duration(minutes: 10)));
}
Loading