zipProject static method

Future<List<int>> zipProject({
  1. required CommandLogger logger,
  2. required Directory rootDirectory,
  3. Iterable<String> beneath = const ['.'],
  4. int fileReadPoolSize = 5,
  5. bool showFiles = false,
  6. Future<String?> fileContentModifier(
    1. String relativePath,
    2. Future<String> contentReader()
    )?,
})

Zips a project directory. Returns a list of bytes representing the zipped project.

The logger is used to log debug information and warnings. The rootDirectory is the directory under which contents will be zipped. The beneath is the list of relative paths under rootDirectory that will be included, all by default. The fileReadPoolSize is the number of files that are processed concurrently. The fileContentModifier is an optional callback that can modify file content before it is added to the archive. It receives the relative path and a content reader function. The callback should return the modified content as a string, or null if no modification is needed (in which case the file will be added as binary). The content reader is only called when the modifier decides it needs to read the file content.

All exceptions thrown by this method are subclasses of ProjectZipperExceptions. Throws ProjectDirectoryDoesNotExistException if the project directory does not exist. Throws EmptyProjectException if the project directory is empty. Throws DirectorySymLinkException if the project directory contains a directory symlink. Throws NonResolvingSymlinkException if the project directory contains a non-resolving symlink.

Implementation

static Future<List<int>> zipProject({
  required final CommandLogger logger,
  required final Directory rootDirectory,
  final Iterable<String> beneath = const ['.'],
  final int fileReadPoolSize = 5,
  final bool showFiles = false,
  final Future<String?> Function(
    String relativePath,
    Future<String> Function() contentReader,
  )?
  fileContentModifier,
}) async {
  final projectPath = rootDirectory.path;

  if (!rootDirectory.existsSync()) {
    throw ProjectDirectoryDoesNotExistException(projectPath);
  }

  final filesToUpload = <String>{};
  final filesIgnored = <String>{};
  for (final b in beneath) {
    final (included, ignored) = ProjectFiles.collectFiles(
      logger: logger,
      rootDirectory: rootDirectory,
      beneath: b,
    );
    filesToUpload.addAll(included);
    filesIgnored.addAll(ignored);
  }

  logger.debug('Found ${filesToUpload.length} files to upload.');
  if (showFiles) {
    FileTreePrinter.writeFileTree(
      filePaths: filesToUpload
          .map((final file) => stripRoot(projectPath, file))
          .toSet(),
      ignoredPaths: filesIgnored
          .map((final file) => stripRoot(projectPath, file))
          .toSet(),
      write: logger.raw,
    );
  }

  final archive = Archive();
  final fileReadPool = Pool(fileReadPoolSize);

  Future<void> addFileToArchive(final String path) async {
    final file = File(path);
    if (!file.existsSync()) return;

    await fileReadPool.withResource(() async {
      final relativePath = stripRoot(projectPath, path);

      List<int> bytes;
      if (fileContentModifier != null) {
        final modifiedContent = await fileContentModifier(
          relativePath,
          () => file.readAsString(),
        );
        if (modifiedContent != null) {
          bytes = utf8.encode(modifiedContent);
        } else {
          bytes = await file.readAsBytes();
        }
      } else {
        bytes = await file.readAsBytes();
      }

      archive.addFile(ArchiveFile(relativePath, bytes.length, bytes));
    });
  }

  await Future.wait(filesToUpload.map(addFileToArchive));

  if (archive.isEmpty) {
    throw const EmptyProjectException();
  }

  final encoded = ZipEncoder().encode(archive);
  logger.debug(
    'Encoded ${archive.length} files to ${_formatFileSize(encoded?.length ?? 0)}.',
  );

  if (encoded == null) {
    // This should never happen.
    // If we end up here, it's a bug in the archive package.
    throw const NullZipException();
  }

  return encoded;
}