Compare commits

..

No commits in common. "master" and "0.5" have entirely different histories.
master ... 0.5

13 changed files with 497 additions and 548 deletions

247
.gitignore vendored
View file

@ -2,11 +2,11 @@
build/
target/
compile_commands.json
.vscode
.ccls-cache
.idea/codeStyles/**
# Created by https://www.toptal.com/developers/gitignore/api/c++,vim,emacs,linux,macos,ninja,windows,jetbrains+all,clion+all,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=c++,vim,emacs,linux,macos,ninja,windows,jetbrains+all,clion+all,visualstudiocode
# Created by https://www.gitignore.io/api/vim,c++,emacs,ninja
# Edit at https://www.gitignore.io/?templates=vim,c++,emacs,ninja
### C++ ###
# Prerequisites
@ -42,94 +42,6 @@ compile_commands.json
*.out
*.app
### CLion+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### CLion+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Emacs ###
# -*- mode: gitignore; -*-
*~
@ -181,108 +93,6 @@ flycheck_*.el
/network-security.data
### JetBrains+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### JetBrains+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
### Linux ###
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Ninja ###
.ninja_deps
.ninja_log
@ -290,7 +100,6 @@ Temporary Items
### Vim ###
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
@ -302,54 +111,14 @@ Sessionx.vim
# Temporary
.netrwhist
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Coc configuration directory
.vim
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/c++,vim,emacs,linux,macos,ninja,windows,jetbrains+all,clion+all,visualstudiocode
# End of https://www.gitignore.io/api/vim,c++,emacs,ninja

View file

@ -1,5 +1,4 @@
#include "Archiver.hpp"
#include "Formats.hpp"
#include <spdlog/spdlog.h>
@ -38,9 +37,9 @@ fs::path archive_extension(const fs::path& path) {
while (tmp_path.has_extension()) {
tmp_ext = tmp_path.extension() += tmp_ext;
Format format = find_extension_format(tmp_ext);
auto search = extensions_format.find(tmp_ext);
if (format != Format::UNKNOWN) {
if (search != extensions_format.end()) {
// (Combined) extension known. Remember as `ext` and keep
// looking for even longer extensions.
ext = tmp_ext;
@ -77,9 +76,9 @@ fs::path strip_archive_extension(const fs::path& path) {
tmp_ext = tmp_path.extension() += tmp_ext;
spdlog::debug("Looking for {} in known extensions", tmp_ext);
Format format = find_extension_format(tmp_ext);
auto search = extensions_format.find(tmp_ext);
tmp_longest_ext++;
if (format != Format::UNKNOWN) {
if (search != extensions_format.end()) {
// (Combined) extension known. Remember as `longest_ext` and keep
// looking for even longer extensions.
longest_ext = tmp_longest_ext;
@ -120,22 +119,19 @@ Format parse_format(const fs::path& path) {
spdlog::debug("Looking for path {}", path);
fs::path ext = archive_extension(path);
spdlog::debug("Looking for ext {}", ext);
Format format = find_extension_format(ext);
if (format == Format::UNKNOWN) {
auto search = extensions_format.find(ext);
if (search == extensions_format.end()) {
throw XwimError{"No known archiver for {}", path};
}
return format;
return search->second;
}
unique_ptr<Archiver> make_archiver(const string& archive_name) {
switch (parse_format(archive_name)) {
case Format::TAR_GZIP: case Format::TAR_BZIP2:
case Format::TAR_COMPRESS: case Format::TAR_LZIP:
case Format::TAR_XZ: case Format::TAR_ZSTD:
case Format::ZIP:
return make_unique<LibArchiver>();
case Format::TAR_GZ:
case Format::ZIP:
return make_unique<LibArchiver>();
default:
throw XwimError{
"Cannot construct archiver for {}. `extension_format` surjection "

View file

@ -8,10 +8,17 @@
#include <set>
#include "util/Common.hpp"
#include "Formats.hpp"
namespace xwim {
// Invariant:
// `extensions_format` defines a surjection from `format_extensions`
// to `Formats`
const std::set<std::string> format_extensions{".tar.gz", ".zip"};
enum class Format { TAR_GZ, ZIP };
const std::map<std::string, Format> extensions_format{
{".tar.gz", Format::TAR_GZ}, {".zip", Format::ZIP}};
class Archiver {
public:
virtual void compress(std::set<std::filesystem::path> ins,

View file

@ -1,49 +0,0 @@
#pragma once
namespace xwim {
using namespace std;
// Invariant:
// `extensions_format` defines a surjection from `format_extensions`
// to `Formats`
enum class Format {
UNKNOWN,
TAR_BZIP2, TAR_GZIP, TAR_LZIP, TAR_XZ, TAR_COMPRESS, TAR_ZSTD,
ZIP
};
const set<string> format_extensions{
// tar formats see: https://en.wikipedia.org/wiki/Tar_(computing)#Suffixes_for_compressed_files
/* bzip2 */ ".tar.bz2", ".tb2", ".tbz", ".tbz2", ".tz2",
/* gzip */ ".tar.gz", ".taz", ".tgz",
/* lzip */ ".tar.lz",
/* xz */ ".tar.xz", ".txz",
/* compress */ ".tar.Z", ".tZ", ".taZ",
/* zstd */ ".tar.zst", ".tzst",
/* zip */ ".zip"
};
const map<set<string>, Format> extensions_format{
{{".tar.bz2", ".tb2", ".tbz", ".tbz2", ".tz2"}, Format::TAR_BZIP2},
{{".tar.gz", ".taz", ".tgz"}, Format::TAR_GZIP},
{{".tar.lz"}, Format::TAR_LZIP},
{{".tar.xz", ".txz"}, Format::TAR_XZ},
{{".tar.Z", ".tZ", ".taZ"}, Format::TAR_COMPRESS},
{{".tar.zst", ".tzst"}, Format::TAR_ZSTD},
{{".zip"}, Format::ZIP}
};
inline Format find_extension_format(const string& ext) {
for(auto ef: extensions_format) {
auto f = ef.first.find(ext);
if(f != ef.first.end()) {
return ef.second;
}
}
return Format::UNKNOWN;
}
}

View file

@ -1,226 +1,190 @@
#include "UserIntent.hpp"
#include <spdlog/spdlog.h>
#include <algorithm>
#include <filesystem>
#include <spdlog/spdlog.h>
#include "Archiver.hpp"
namespace xwim {
unique_ptr<UserIntent> make_compress_intent(const UserOpt &userOpt) {
if (userOpt.paths.size() == 1) {
return make_unique<CompressSingleIntent>(
CompressSingleIntent{*userOpt.paths.begin(), userOpt.out});
}
if (!userOpt.out.has_value()) {
throw XwimError("Cannot guess output for multiple targets");
}
unique_ptr<UserIntent> make_intent(const UserOpt &userOpt) {
if (userOpt.wants_compress() && userOpt.wants_extract()) {
throw XwimError("Cannot compress and extract simultaneously");
}
if(userOpt.paths.size() == 0) {
throw XwimError("No input given...");
}
return make_unique<CompressManyIntent>(
CompressManyIntent{userOpt.paths, userOpt.out.value()});
}
// compression intent explicitly specified
if (userOpt.wants_compress()) {
if (userOpt.paths.size() == 1) {
return make_unique<CompressSingleIntent>(
CompressSingleIntent{
*userOpt.paths.begin(),
userOpt.out
});
}
unique_ptr<UserIntent> make_extract_intent(const UserOpt &userOpt) {
for (const path &p : userOpt.paths) {
if (!can_handle_archive(p)) {
throw XwimError("Cannot extract path {}", p);
}
}
if (!userOpt.out.has_value()) {
throw XwimError("Cannot guess output for multiple targets");
}
return make_unique<ExtractIntent>(ExtractIntent{userOpt.paths, userOpt.out});
}
return make_unique<CompressManyIntent>(
CompressManyIntent{
userOpt.paths,
userOpt.out.value()
});
}
unique_ptr<UserIntent> try_infer_compress_intent(const UserOpt &userOpt) {
if (!userOpt.out.has_value()) {
spdlog::debug("No <out> provided");
if (userOpt.paths.size() != 1) {
spdlog::debug(
"Not a single-path compression. Cannot guess <out> for many-path "
"compression");
return nullptr;
// extraction intent explicitly specified
if (userOpt.wants_extract()) {
for (path p: userOpt.paths) {
if (!can_handle_archive(p)) {
throw XwimError("Cannot extract path {}", p);
}
}
return make_unique<ExtractIntent>(
ExtractIntent{
userOpt.paths,
userOpt.out
});
}
// no intent explicitly specified, try to infer from input
bool can_extract_all = std::all_of(
userOpt.paths.begin(), userOpt.paths.end(),
[](path path) {
return can_handle_archive(path);
});
bool is_out_archive = userOpt.out.has_value() && can_handle_archive(userOpt.out.value());
// out is explicitly specified and an archive, assume we want compression
if(is_out_archive) {
if(userOpt.paths.size() == 1) {
return make_unique<CompressSingleIntent>(
CompressSingleIntent{
*userOpt.paths.begin(),
userOpt.out
});
}
return make_unique<CompressManyIntent>(
CompressManyIntent{
userOpt.paths,
userOpt.out.value() // this is ok is_out_archive checks for has_value()
}
);
}
// all inputs are extractable archives, assume extraction intent
if (can_extract_all) {
return make_unique<ExtractIntent>(
ExtractIntent{
userOpt.paths,
userOpt.out
});
}
// at this point all we can hope for is that the intention is a single-path compression:
// we don't know how to extract it; we don't know (and can't guess) output for many-path compression;
if(userOpt.paths.size() == 1) {
return make_unique<CompressSingleIntent>(
CompressSingleIntent{
*userOpt.paths.begin(),
userOpt.out
});
}
throw XwimError("Cannot guess intent");
}
spdlog::debug("Only one <path> provided. Assume single-path compression.");
return make_unique<CompressSingleIntent>(
CompressSingleIntent{*userOpt.paths.begin(), userOpt.out});
}
spdlog::debug("<out> provided: {}", userOpt.out.value());
if (can_handle_archive(userOpt.out.value())) {
spdlog::debug("{} given and a known archive format, assume compression",
userOpt.out.value());
return make_compress_intent(userOpt);
}
void ExtractIntent::execute() {
bool has_out = this->out.has_value();
bool is_single = this->archives.size() == 1;
spdlog::debug(
"Cannot compress multiple paths without a user-provided output archive");
return nullptr;
}
for (path p: this->archives) {
unique_ptr<Archiver> archiver = make_archiver(p);
path out;
unique_ptr<UserIntent> try_infer_extract_intent(const UserOpt &userOpt) {
bool can_extract_all =
std::all_of(userOpt.paths.begin(), userOpt.paths.end(),
[](const path &path) { return can_handle_archive(path); });
if(has_out) {
if(is_single) { // just dump content of archive into `out`
std::filesystem::create_directories(this->out.value());
out = this->out.value();
} else { // create an `out` folder and extract inside there
std::filesystem::create_directories(this->out.value());
out = this->out.value() / strip_archive_extension(p);
}
} else {
out = std::filesystem::current_path() / strip_archive_extension(p);
std::filesystem::create_directories(out);
}
if (!can_extract_all) {
spdlog::debug(
"Cannot extract all provided <paths>. Assume this is not an "
"extraction.");
for (const path &p : userOpt.paths) {
if (!can_handle_archive(p)) {
spdlog::debug("Cannot handle {}", p);
}
}
archiver->extract(p, out);
return nullptr;
}
// move folder if only one entry and that entries name is already
// the stripped archive name
auto dit = std::filesystem::directory_iterator(out);
if (userOpt.out.has_value() && can_handle_archive(userOpt.out.value())) {
spdlog::debug(
"Could extract all provided <paths>. But also {} looks like an "
"archive. Ambiguous intent. Assume this is not an extraction.",
userOpt.out.value());
return nullptr;
}
if(dit == std::filesystem::directory_iterator()) {
spdlog::debug("Archive is empty");
} else if(is_directory(dit->path())){
auto first_path = dit->path();
auto next_entry = next(dit);
spdlog::debug(
"Could extract all provided <paths>. But also <out> looks like an "
"archive. Ambiguous intent. Assume this is not an extraction.");
return make_extract_intent(userOpt);
}
if(next_entry == std::filesystem::directory_iterator()) {
spdlog::debug("Archive has single entry which is a directory");
if(std::filesystem::equivalent(first_path.filename(), out.filename())) {
spdlog::debug("Archive entry named like archive");
int i = rand_int(0, 100000);
unique_ptr<UserIntent> make_intent(const UserOpt &userOpt) {
if (userOpt.wants_compress() && userOpt.wants_extract()) {
throw XwimError("Cannot compress and extract simultaneously");
}
if (userOpt.paths.empty()) {
throw XwimError("No input given...");
}
path tmp_out = path{out};
tmp_out.concat(fmt::format(".xwim{}", i));
// explicitly specified intent
if (userOpt.wants_compress()) return make_compress_intent(userOpt);
if (userOpt.wants_extract()) return make_extract_intent(userOpt);
spdlog::debug("Moving {} to {}", first_path, tmp_out);
std::filesystem::rename(first_path, tmp_out);
spdlog::debug("Removing parent {}", out);
std::filesystem::remove(out);
spdlog::debug("Moving {} to {}", tmp_out, out);
std::filesystem::rename(tmp_out, out);
spdlog::info("Intent not explicitly provided, trying to infer intent");
} else {
spdlog::debug("Archive entry differs from archive name");
}
} else {
spdlog::debug("Archive has multiple entries");
}
}
}
};
if (auto intent = try_infer_extract_intent(userOpt)) {
spdlog::info("Extraction intent inferred");
return intent;
}
spdlog::info("Cannot infer extraction intent");
void CompressSingleIntent::execute() {
if(this->out.has_value()) {
if(!can_handle_archive(this->out.value())) {
throw XwimError("Unknown archive format {}", this->out.value());
}
if (auto intent = try_infer_compress_intent(userOpt)) {
spdlog::info("Compression intent inferred");
return intent;
}
spdlog::info("Cannot infer compression intent");
unique_ptr<Archiver> archiver = make_archiver(this->out.value());
set<path> ins{this->in};
archiver->compress(ins, this->out.value());
} else {
path out = default_archive(strip_archive_extension(this->in).stem());
unique_ptr<Archiver> archiver = make_archiver(out);
set<path> ins{this->in};
archiver->compress(ins, out);
}
};
throw XwimError("Cannot guess intent");
}
void CompressManyIntent::execute() {
if(!can_handle_archive(this->out)) {
throw XwimError("Unknown archive format {}", this->out);
}
void ExtractIntent::dwim_reparent(const path &out) {
// move extraction if extraction resulted in only one entry and that entries
// name is already the stripped archive name, i.e. reduce unnecessary nesting
auto dit = std::filesystem::directory_iterator(out);
auto dit_path = dit->path();
if (dit == std::filesystem::directory_iterator()) {
spdlog::debug(
"Cannot flatten extraction folder: extraction folder is empty");
return;
}
if (!is_directory(dit_path)) {
spdlog::debug("Cannot flatten extraction folder: {} is not a directory",
dit_path);
return;
}
if (next(dit) != std::filesystem::directory_iterator()) {
spdlog::debug("Cannot flatten extraction folder: multiple items extracted");
return;
}
if (!std::filesystem::equivalent(dit_path.filename(), out.filename())) {
spdlog::debug(
"Cannot flatten extraction folder: archive entry differs from archive "
"name [extraction folder: {}, archive entry: {}]",
out.filename(), dit_path.filename());
return;
}
spdlog::debug("Output folder [{}] is equivalent to archive entry [{}]", out,
dit_path);
spdlog::info("Flattening extraction folder");
int i = rand_int(0, 100000);
path tmp_out = path{out};
tmp_out.concat(fmt::format(".xwim{}", i));
spdlog::debug("Move {} to {}", dit_path, tmp_out);
std::filesystem::rename(dit_path, tmp_out);
spdlog::debug("Remove parent path {}", out);
std::filesystem::remove(out);
spdlog::debug("Moving {} to {}", tmp_out, out);
std::filesystem::rename(tmp_out, out);
}
path ExtractIntent::out_path(const path &p) {
if (!this->out.has_value()) {
// not out path given, create from archive name
path out = std::filesystem::current_path() / strip_archive_extension(p);
create_directories(out);
return out;
}
if (this->archives.size() == 1) {
// out given and only one archive to extract, just extract into `out`
create_directories(this->out.value());
return this->out.value();
}
// out given and multiple archives to extract, create subfolder
// for each archive
create_directories(this->out.value());
path out = this->out.value() / strip_archive_extension(p);
return out;
}
void ExtractIntent::execute() {
for (const path &p : this->archives) {
std::unique_ptr<Archiver> archiver = make_archiver(p);
path out = this->out_path(p);
archiver->extract(p, out);
this->dwim_reparent(out);
}
}
path CompressSingleIntent::out_path() {
if (this->out.has_value()) {
if (!can_handle_archive(this->out.value())) {
throw XwimError("Unknown archive format {}", this->out.value());
}
return this->out.value();
}
return default_archive(strip_archive_extension(this->in).stem());
}
void CompressSingleIntent::execute() {
path out = this->out_path();
unique_ptr<Archiver> archiver = make_archiver(out);
set<path> ins{this->in};
archiver->compress(ins, out);
};
void CompressManyIntent::execute() {
if (!can_handle_archive(this->out)) {
throw XwimError("Unknown archive format {}", this->out);
}
unique_ptr<Archiver> archiver = make_archiver(this->out);
archiver->compress(this->in_paths, this->out);
}
unique_ptr<Archiver> archiver = make_archiver(this->out);
archiver->compress(this->in_paths, this->out);
};
} // namespace xwim

View file

@ -30,14 +30,11 @@ private:
set<path> archives;
optional<path> out;
void dwim_reparent(const path& out);
path out_path(const path& p);
public:
public:
ExtractIntent(set<path> archives, optional<path> out): archives(archives), out(out) {};
~ExtractIntent() override = default;
~ExtractIntent() = default;
void execute() override;
void execute();
};
/**
@ -58,15 +55,14 @@ private:
class CompressSingleIntent : public UserIntent {
private:
path in;
optional<path> out;
path out_path();
optional <path> out;
public:
CompressSingleIntent(path in, optional<path> out) : UserIntent(), in(in), out(out) {};
~CompressSingleIntent() override = default;
CompressSingleIntent(path in, optional <path> out) : UserIntent(), in(in), out(out) {};
void execute() override;
~CompressSingleIntent() = default;
void execute();
};
/**
@ -85,9 +81,9 @@ private:
public:
CompressManyIntent(set<path> in_paths, path out): UserIntent(), in_paths(in_paths), out(out) {};
~CompressManyIntent() override = default;
~CompressManyIntent() = default;
void execute() override;
void execute();
};
} // namespace xwim

View file

@ -1,6 +1,11 @@
#include "UserOpt.hpp"
#include <tclap/ArgException.h>
#include <tclap/CmdLine.h>
#include <tclap/StdOutput.h>
#include <tclap/SwitchArg.h>
#include <tclap/UnlabeledMultiArg.h>
#include <tclap/ValueArg.h>
template <>
struct TCLAP::ArgTraits<std::filesystem::path> {
@ -28,9 +33,6 @@ UserOpt::UserOpt(int argc, char** argv) {
TCLAP::ValueArg<fs::path> arg_outfile
{"o", "out", "Out <file-or-path>", false, fs::path{}, "A path on the filesystem", cmd};
TCLAP::MultiSwitchArg arg_verbose
{"v", "verbose", "Verbosity level", cmd, 0};
TCLAP::UnlabeledMultiArg<fs::path> arg_paths
{"files", "Archive(s) to extract or file(s) to compress", true, "A path on the filesystem", cmd};
// clang-format on
@ -41,8 +43,7 @@ UserOpt::UserOpt(int argc, char** argv) {
if (arg_extract.isSet()) this->extract = arg_extract.getValue();
if (arg_outfile.isSet()) this->out = arg_outfile.getValue();
this->verbosity = arg_verbose.getValue();
this->interactive = !arg_noninteractive.getValue();
this->interactive = arg_extract.getValue();
if (arg_paths.isSet()) {
this->paths =

View file

@ -13,7 +13,6 @@ struct UserOpt {
optional<bool> compress;
optional<bool> extract;
bool interactive;
int verbosity;
std::optional<fs::path> out;
std::set<fs::path> paths;

165
src/Xwim.cpp Normal file
View file

@ -0,0 +1,165 @@
#include "Xwim.hpp"
#include <spdlog/spdlog.h>
#include <algorithm>
#include <cstdlib>
#include <filesystem>
#include <ios>
#include <iostream>
#include <iterator>
#include <random>
#include <string>
#include "Archiver.hpp"
#include "util/Common.hpp"
namespace xwim {
using namespace std;
namespace fs = std::filesystem;
#if defined(unix) || defined(__unix__) || defined(__unix)
std::string default_extension = ".tar.gz";
#elif defined(_win32) || defined(__win32__) || defined(__windows__)
std::string default_extension = ".zip";
#else
std::string default_extension = ".zip";
#endif
Xwim::Xwim() : action{Action::UNKNOWN} {}
void Xwim::try_infer() {
infer_action();
infer_output();
if (action == Action::COMPRESS) {
archiver = make_archiver(out.string());
} else if (action == Action::EXTRACT) {
// we can only handle one archive for extraction at a time.
// Checked in `infer_extraction_output`
archiver = make_archiver(ins.begin()->string());
}
}
void Xwim::dwim() {
switch (action) {
case Action::COMPRESS:
this->archiver->compress(ins, out);
break;
case Action::EXTRACT:
this->archiver->extract(*ins.begin(), out);
sanitize_output();
break;
default:
spdlog::error("Unknown action");
}
}
void Xwim::sanitize_output() {
fs::path in_stripped = xwim::strip_archive_extension(*ins.begin());
int count = 0;
fs::directory_entry first_entry;
for(auto& e: fs::directory_iterator(out)) {
count++;
if(first_entry.path().empty()) {
first_entry = e;
}
}
if (count >= 2) {
spdlog::debug("Found multiple entries in extraction directory. Moving {} to {}", out, in_stripped);
fs::rename(out, in_stripped);
} else {
if(first_entry.is_directory()) {
spdlog::debug("Found single directory in extraction directory. Moving {} to {}",
first_entry.path(), in_stripped);
fs::rename(first_entry, in_stripped);
fs::remove(out);
} else {
spdlog::debug(
"Found single file in extraction directory. Moving {} to {}", out, in_stripped);
fs::rename(out, in_stripped);
}
}
}
void Xwim::infer_action() {
if (action != Action::UNKNOWN) return;
if (ins.size() == 1 && can_extract(*ins.begin())) {
action = Action::EXTRACT;
} else {
action = Action::COMPRESS;
}
spdlog::debug("Inferred action: {}", action);
}
void Xwim::infer_output() {
if (!out.empty()) return;
switch (action) {
case Action::COMPRESS:
infer_compression_output();
break;
case Action::EXTRACT:
infer_extraction_output();
break;
default:
throw XwimError{"Cannot infer output, action is unknown"};
}
spdlog::debug("Inferred out: {}", out.string());
}
void Xwim::infer_compression_output() {
if (ins.size() == 1) {
// archive name is just the name of the input with default archive
// extension
fs::path archive_stem = xwim::strip_archive_extension(*ins.begin());
archive_stem += default_extension;
out = archive_stem;
} else {
// We cannot guess the name of the output archive
// TODO use readline/lineoise/editline for path completion
cout << "Archive name: ";
cin >> out;
out = fs::path(out);
}
}
void Xwim::infer_extraction_output() {
if (ins.size() > 1) {
throw XwimError{"Cannot extract more than one archive at a time"};
}
// create a temporary path for extraction
fs::path archive_stem = xwim::strip_archive_extension(*ins.begin());
// note: we use here what is considered an `extensions` by `fs::path` so that
// we can strip it again easily later on
archive_stem += ".";
archive_stem += to_string(rand_int(999, 99999));
archive_stem += ".tmp";
this->out = archive_stem;
}
void Xwim::setCompress() {
this->action = Action::COMPRESS;
spdlog::debug("Set action to {}", this->action);
}
void Xwim::setExtract() {
this->action = Action::EXTRACT;
spdlog::debug("Set action to {}", this->action);
}
void Xwim::setOut(fs::path path) {
this->out = path;
spdlog::debug("Set out to {}", this->out);
}
void Xwim::setIns(vector<fs::path> ins) {
this->ins.insert(ins.begin(), ins.end());
if (this->ins.size() != ins.size()) {
spdlog::warn("Duplicate input files found. Removed {} duplicate(s).",
(ins.size() - this->ins.size()));
}
}
} // namespace xwim

106
src/Xwim.hpp Normal file
View file

@ -0,0 +1,106 @@
#pragma once
#include <fmt/core.h>
#include <fmt/format.h>
#include <exception>
#include <memory>
#include <optional>
#include <set>
#include <stdexcept>
#include "Archiver.hpp"
#include "util/Common.hpp"
#include "UserOpt.hpp"
namespace xwim {
using namespace std;
namespace fs = std::filesystem;
enum class Action { EXTRACT, COMPRESS };
struct XwimIntent {
};
class XwimBuilder {
private:
UserOpt user_opt;
public:
XwimBuilder(UserOpt user_opt) : user_opt(user_opt){};
Xwim build();
};
class Xwim {
public:
virtual XwimResult dwim() = 0;
};
class XwimCompressor : public Xwim {
private:
fs::path archive;
std::set<fs::path> paths;
};
class XwimExtractor : public Xwim {};
class XwimConfig {
public:
Action get_action();
}
class Xwim {
private:
XwimEngine xwim_engine;
UserOpt user_opt;
public:
Xwim(UserOpt user_opt);
void dwim();
}
class Xwim {
private:
Action action;
fs::path out;
set<fs::path> ins;
unique_ptr<Archiver> archiver;
void infer_action();
void infer_output();
void infer_compression_output();
void infer_extraction_output();
void sanitize_output();
public:
Xwim();
void try_infer();
void dwim();
void setCompress();
void setExtract();
void setOut(fs::path);
void setIns(vector<fs::path> ins);
};
} // namespace xwim
template <>
struct fmt::formatter<xwim::Action> {
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
template <typename FormatContext>
auto format(const xwim::Action& action, FormatContext& ctx) {
switch (action) {
case xwim::Action::UNKNOWN:
return format_to(ctx.out(), "UNKNOWN");
case xwim::Action::EXTRACT:
return format_to(ctx.out(), "EXTRACT");
case xwim::Action::COMPRESS:
return format_to(ctx.out(), "COMPRESS");
};
return format_to(ctx.out(), "");
}
};

15
src/XwimConfig.hpp Normal file
View file

@ -0,0 +1,15 @@
#pragma once
namespace xwim {
enum class Action { COMPRESS, EXTRACT };
class XwimConfig {
public:
Action get_action();
}
}

View file

@ -1,11 +1,13 @@
#include <spdlog/common.h>
#include <spdlog/logger.h>
#include <spdlog/spdlog.h>
#include <cstdlib>
#include <filesystem>
#include <optional>
#include "UserIntent.hpp"
#include "UserOpt.hpp"
#include "UserIntent.hpp"
#include "util/Common.hpp"
#include "util/Log.hpp"
@ -14,13 +16,12 @@ using namespace std;
int main(int argc, char** argv) {
log::init();
UserOpt user_opt = UserOpt{argc, argv};
log::init(user_opt.verbosity);
UserOpt user_opt = UserOpt{argc, argv};
try {
unique_ptr<UserIntent> user_intent = make_intent(user_opt);
user_intent->execute();
} catch (XwimError& e) {
spdlog::error(e.what());
unique_ptr<UserIntent> user_intent = make_intent(user_opt);
user_intent->execute();
} catch(XwimError& e) {
spdlog::error(e.what());
}
}

View file

@ -1,7 +1,6 @@
#pragma once
#include <spdlog/common.h>
#include <spdlog/spdlog.h>
#include <cstdlib>
#ifdef NDEBUG
#define XWIM_LOGLEVEL SPDLOG_LEVEL_ERROR
@ -58,27 +57,7 @@ spdlog::level::level_enum _init_from_compile() {
* The determined level is then set for the default logger via
* `spdlog::set_level`.
*/
void init(int verbosity = -1,
spdlog::level::level_enum level = spdlog::level::level_enum::off) {
if (verbosity != -1) {
switch (verbosity) {
case 0:
spdlog::set_level(spdlog::level::off);
break;
case 1:
spdlog::set_level(spdlog::level::info);
break;
case 2:
spdlog::set_level(spdlog::level::debug);
break;
case 3:
default:
spdlog::set_level(spdlog::level::trace);
break;
}
return;
}
void init(spdlog::level::level_enum level = spdlog::level::level_enum::off) {
if (spdlog::level::level_enum::off != level) {
spdlog::set_level(level);
return;