diff --git a/.drone.yml b/.drone.yml index 87372ab..5d9245e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -6,7 +6,7 @@ steps: - name: build-shared image: arminfriedl/xwim-build:shared commands: - - meson wrap install gtest + - meson wrap install gtest || true - meson target/shared - ninja -C target/shared - mv target/shared/src/xwim xwim-x86_64-glibc-linux-shared @@ -14,7 +14,7 @@ steps: - name: build-static image: arminfriedl/xwim-build:static commands: - - meson wrap install gtest + - meson wrap install gtest || true - meson --default-library=static target/static - ninja -C target/static - mv target/static/src/xwim xwim-x86_64-musl-linux-static @@ -50,6 +50,7 @@ steps: - name: build-shared image: arminfriedl/xwim-build:shared commands: + - meson wrap install gtest || true - meson --buildtype=release target/shared - ninja -C target/shared - strip target/shared/src/xwim @@ -59,6 +60,7 @@ steps: - name: build-static image: arminfriedl/xwim-build:static commands: + - meson wrap install gtest || true - meson --buildtype=release --default-library=static target/static - ninja -C target/static - strip target/static/src/xwim diff --git a/src/Archiver.cpp b/src/Archiver.cpp index a29960d..7f2214e 100644 --- a/src/Archiver.cpp +++ b/src/Archiver.cpp @@ -1,6 +1,5 @@ #include "Archiver.hpp" -#include #include #include @@ -9,6 +8,14 @@ #include "util/Common.hpp" +#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 + namespace xwim { using namespace std; namespace fs = std::filesystem; @@ -90,7 +97,14 @@ fs::path strip_archive_extension(const fs::path& path) { return tmp_path; } -bool can_extract(const fs::path& path) { +std::filesystem::path default_archive(const std::filesystem::path& base) { + string base_s = base.string(); + string ext_s = default_extension; + + return fs::path{fmt::format("{}{}", base_s, ext_s)}; +} + +bool can_handle_archive(const fs::path& path) { fs::path ext = archive_extension(path); if (format_extensions.find(ext.string()) != format_extensions.end()) { spdlog::debug("Found {} in known formats", ext); @@ -125,4 +139,5 @@ unique_ptr make_archiver(const string& archive_name) { archive_name}; }; } + } // namespace xwim diff --git a/src/Archiver.hpp b/src/Archiver.hpp index 9d07848..430f442 100644 --- a/src/Archiver.hpp +++ b/src/Archiver.hpp @@ -40,8 +40,10 @@ class LibArchiver : public Archiver { std::filesystem::path archive_extension(const std::filesystem::path& path); std::filesystem::path strip_archive_extension(const std::filesystem::path& path); +std::filesystem::path default_archive(const std::filesystem::path& base); + Format parse_format(const std::filesystem::path& path); -bool can_extract(const std::filesystem::path& path); +bool can_handle_archive(const std::filesystem::path& path); std::unique_ptr make_archiver(const std::string& archive_name); diff --git a/src/UserIntent.cpp b/src/UserIntent.cpp new file mode 100644 index 0000000..ed0f4b5 --- /dev/null +++ b/src/UserIntent.cpp @@ -0,0 +1,190 @@ +#include "UserIntent.hpp" + +#include +#include +#include + +#include "Archiver.hpp" + +namespace xwim { + + unique_ptr 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..."); + } + + // compression intent explicitly specified + if (userOpt.wants_compress()) { + if (userOpt.paths.size() == 1) { + return make_unique( + CompressSingleIntent{ + *userOpt.paths.begin(), + userOpt.out + }); + } + + if (!userOpt.out.has_value()) { + throw XwimError("Cannot guess output for multiple targets"); + } + + return make_unique( + CompressManyIntent{ + userOpt.paths, + userOpt.out.value() + }); + } + + // 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{ + 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{ + *userOpt.paths.begin(), + userOpt.out + }); + } + + return make_unique( + 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{ + 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{ + *userOpt.paths.begin(), + userOpt.out + }); + } + + throw XwimError("Cannot guess intent"); + } + + + void ExtractIntent::execute() { + bool has_out = this->out.has_value(); + bool is_single = this->archives.size() == 1; + + for (path p: this->archives) { + unique_ptr archiver = make_archiver(p); + path out; + + 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); + } + + archiver->extract(p, out); + + // move folder if only one entry and that entries name is already + // the stripped archive name + auto dit = std::filesystem::directory_iterator(out); + + 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); + + 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); + + path tmp_out = path{out}; + tmp_out.concat(fmt::format(".xwim{}", i)); + + 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); + + } else { + spdlog::debug("Archive entry differs from archive name"); + } + } else { + spdlog::debug("Archive has multiple entries"); + } + } + } + }; + + void CompressSingleIntent::execute() { + if(this->out.has_value()) { + if(!can_handle_archive(this->out.value())) { + throw XwimError("Unknown archive format {}", this->out.value()); + } + + unique_ptr archiver = make_archiver(this->out.value()); + set ins{this->in}; + archiver->compress(ins, this->out.value()); + } else { + path out = default_archive(strip_archive_extension(this->in).stem()); + unique_ptr archiver = make_archiver(out); + set 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 = make_archiver(this->out); + archiver->compress(this->in_paths, this->out); + }; +} // namespace xwim diff --git a/src/UserIntent.hpp b/src/UserIntent.hpp new file mode 100644 index 0000000..567050d --- /dev/null +++ b/src/UserIntent.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include +#include + +#include "util/Common.hpp" +#include "UserOpt.hpp" + +namespace xwim { +using namespace std; +using std::filesystem::path; + +class UserIntent { +public: + virtual void execute() = 0; + virtual ~UserIntent() = default; +}; + +/* Factory method to construct a UserIntent which implements `execute()` */ +unique_ptr make_intent(const UserOpt& userOpt); + +/** +* Extraction intent +* +* Extracts one or multiple archives. Optionally extracts them to given `out` folder. Otherwise extracts them to the +* current working directory. +*/ +class ExtractIntent: public UserIntent { +private: + set archives; + optional out; + +public: + ExtractIntent(set archives, optional out): archives(archives), out(out) {}; + ~ExtractIntent() = default; + + void execute(); +}; + +/** +* Compress intent for a single file or folder. +* +* Compresses a single path which may be a file or a folder. +* +* No `out` path given: +* - derives the archive name from the input path +* - uses the default archive format for the platform +* +* `out` path given: +* - `out` path must be a path with a valid archive name (including extension) +* - tries to compress the input to the out archive +* - if the `out` base name is different from the input base name, puts the input into a new folder +* with base name inside the archive (archive base name is always the name of the archive content) +*/ +class CompressSingleIntent : public UserIntent { +private: + path in; + optional out; + +public: + CompressSingleIntent(path in, optional out) : UserIntent(), in(in), out(out) {}; + + ~CompressSingleIntent() = default; + + void execute(); +}; + +/** + * Compress intent for multiple files and/or folders. + * + * Compresses multiple files and/or folders to a single archive as given by the `out` path. Since `out` cannot be + * guessed from the input in this case it is mandatory. + * + * A new, single root folder with base name equal to base name of the `out` archive is created inside the archive. All + * input files are put into this root folder. + */ +class CompressManyIntent: public UserIntent { +private: + set in_paths; + path out; + +public: + CompressManyIntent(set in_paths, path out): UserIntent(), in_paths(in_paths), out(out) {}; + ~CompressManyIntent() = default; + + void execute(); +}; + +} // namespace xwim diff --git a/src/UserOpt.cpp b/src/UserOpt.cpp index 8d57261..06cc160 100644 --- a/src/UserOpt.cpp +++ b/src/UserOpt.cpp @@ -34,16 +34,17 @@ UserOpt::UserOpt(int argc, char** argv) { {"o", "out", "Out ", false, fs::path{}, "A path on the filesystem", cmd}; TCLAP::UnlabeledMultiArg arg_paths - {"files", "Archive to extract or files to compress", true, "A path on the filesystem", cmd}; + {"files", "Archive(s) to extract or file(s) to compress", true, "A path on the filesystem", cmd}; // clang-format on cmd.parse(argc, argv); - this->compress = arg_compress.getValue(); - this->extract = arg_extract.getValue(); - this->interactive = arg_extract.getValue(); + if (arg_compress.isSet()) this->compress = arg_compress.getValue(); + if (arg_extract.isSet()) this->extract = arg_extract.getValue(); if (arg_outfile.isSet()) this->out = arg_outfile.getValue(); + this->interactive = arg_extract.getValue(); + if (arg_paths.isSet()) { this->paths = set{arg_paths.getValue().begin(), arg_paths.getValue().end()}; diff --git a/src/UserOpt.hpp b/src/UserOpt.hpp index a1058fc..8be169f 100644 --- a/src/UserOpt.hpp +++ b/src/UserOpt.hpp @@ -10,13 +10,21 @@ using namespace std; namespace fs = std::filesystem; struct UserOpt { - bool compress; - bool extract; + optional compress; + optional extract; bool interactive; std::optional out; std::set paths; UserOpt(int argc, char** argv); + + bool wants_compress() const { + return this->compress.has_value() && this->compress.value(); + } + + bool wants_extract() const { + return this->extract.has_value() && this->extract.value(); + } }; } // namespace xwim diff --git a/src/Xwim.hpp b/src/Xwim.hpp index 3cc9bdb..665e770 100644 --- a/src/Xwim.hpp +++ b/src/Xwim.hpp @@ -17,12 +17,11 @@ namespace xwim { using namespace std; namespace fs = std::filesystem; - enum class Action { EXTRACT, COMPRESS }; +enum class Action { EXTRACT, COMPRESS }; - struct XwimIntent { +struct XwimIntent { - }; - +}; class XwimBuilder { private: diff --git a/src/XwimIntent.cpp b/src/XwimIntent.cpp deleted file mode 100644 index 7e05539..0000000 --- a/src/XwimIntent.cpp +++ /dev/null @@ -1,122 +0,0 @@ -#include "XwimIntent.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "Archiver.hpp" - -template <> -struct::TCLAP::ArgTraits { - // `operator=` here for path construction because `operator>>` - // (`ValueLike`) causes a split at whitespace - typedef StringLike ValueCategory; -}; - -namespace xwim { - -void UserOpt::parse_args(int argc, char** argv) { - // clang-format off - // TODO: read version from -DVERSION during compilation - TCLAP::CmdLine cmd {"xwim - Do What I Mean Extractor", ' ', "0.3.0"}; - - TCLAP::SwitchArg arg_compress - {"c", "compress", "Compress ", cmd, false}; - - TCLAP::SwitchArg arg_extract - {"x", "extract", "Extract ", cmd, false}; - - TCLAP::SwitchArg arg_noninteractive - {"i", "non-interactive", "Non-interactive, fail on ambiguity", cmd, false}; - - TCLAP::ValueArg arg_outfile - {"o", "out", "Out ", false, path{}, "A path on the filesystem", cmd}; - - TCLAP::UnlabeledMultiArg arg_paths - {"files", "Archive to extract or files to compress", true, "A path on the filesystem", cmd}; - // clang-format on - - // TODO: ideally we'd make sure during parsing that compress and extract - // cannot both be true - - cmd.parse(argc, argv); - - // clang-format off - - // Only set things if they are actually parsed from args. Otherwise we'd - // override settings set through other means, e.g. config files - if (arg_compress.isSet()) { this->compress = arg_compress.getValue(); } - if (arg_extract.isSet()) { this->extract = arg_extract.getValue(); } - if (arg_noninteractive.isSet()) { this->interactive = !arg_noninteractive.getValue(); } - if (arg_outfile.isSet()) { this->out = arg_outfile.getValue(); } - if (arg_paths.isSet()) { this->paths = arg_paths.getValue(); } - - // clang-format on -} - -void UserOpt::parse_config(path config) { // TODO - spdlog::warn("Config parsing is not implemented"); - return; -} - -UserIntent UserOpt::guess_intent() { - return UserIntent{action_intent(), out_intent(), paths_intent()}; -} - -Action UserOpt::action_intent() { - if (compress && extract) { - throw XwimError("Cannot compress and extract simultaneously"); - } - - if (compress) return Action::COMPRESS; - if (extract) return Action::EXTRACT; - - bool can_extract_all = std::all_of( - paths.begin(), paths.end(), [](path path) { return can_extract(path); }); - - if (can_extract_all && !out) { - return Action::EXTRACT; - } // else if can_extract_all && !is_archive(out) -> EXTRACT - - if (!can_extract_all && out /* && is_archive(out) */) { - return Action::COMPRESS; - } - - if (interactive) { - std::cout << "Do you want to compress (y/n)? [y] "; - char c; - std::cin >> c; - - if (c != 'y' && c != 'n' && c != '\n') { - throw XwimError("Cannot guess action. Please answer 'y' or 'n'."); - } - - if (c == 'y' || c == '\n') { - return Action::COMPRESS; - } else if (c == 'n') { - return Action::EXTRACT; - } - } - - throw XwimError("Cannot guess action (compress/extract)"); -} - -path UserOpt::out_intent() { - -} - -set UserOpt::paths_intent() { - -} - -} // namespace xwim diff --git a/src/XwimIntent.hpp b/src/XwimIntent.hpp deleted file mode 100644 index bbd5a95..0000000 --- a/src/XwimIntent.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include - -#include "util/Common.hpp" - -namespace xwim { -using namespace std; -using std::filesystem::path; - -enum class Action { COMPRESS, EXTRACT }; -struct UserIntent { - Action action; - path out; - set paths; -}; - -class UserOpt { -private: - bool compress = true; - bool extract = false; - bool interactive = true; - optional out = nullopt; - vector paths = std::vector{}; - - Action action_intent(); - path out_intent(); - set paths_intent(); - - public: - void parse_config(path config); - void parse_args(int argc, char** argv); - - UserIntent guess_intent(); -}; - -} // namespace xwim diff --git a/src/archiver/LibArchiver.cpp b/src/archiver/LibArchiver.cpp index ab0e012..1ce507d 100644 --- a/src/archiver/LibArchiver.cpp +++ b/src/archiver/LibArchiver.cpp @@ -27,8 +27,9 @@ void LibArchiver::compress(set ins, fs::path archive_out) { // complete type. `archive` is forward declared only. shared_ptr writer; writer = shared_ptr(archive_write_new(), archive_write_free); - archive_write_add_filter_gzip(writer.get()); - archive_write_set_format_pax_restricted(writer.get()); +// archive_write_add_filter_gzip(writer.get()); +// archive_write_set_format_pax_restricted(writer.get()); + archive_write_set_format_filter_by_ext(writer.get(), archive_out.c_str()); archive_write_open_filename(writer.get(), archive_out.c_str()); shared_ptr reader; diff --git a/src/main.cpp b/src/main.cpp index 597622f..fdd9681 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,7 @@ #include #include "UserOpt.hpp" +#include "UserIntent.hpp" #include "util/Common.hpp" #include "util/Log.hpp" @@ -17,4 +18,10 @@ int main(int argc, char** argv) { log::init(); UserOpt user_opt = UserOpt{argc, argv}; -} + try { + unique_ptr user_intent = make_intent(user_opt); + user_intent->execute(); + } catch(XwimError& e) { + spdlog::error(e.what()); + } +} \ No newline at end of file diff --git a/src/meson.build b/src/meson.build index a0cbead..3d19178 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,4 +1,5 @@ -xwim_src = ['main.cpp', 'Xwim.cpp', 'Archiver.cpp', 'UserOpt.cpp'] +xwim_src = ['main.cpp', 'Archiver.cpp', 'UserOpt.cpp', 'UserIntent.cpp'] + xwim_archiver = ['archiver/LibArchiver.cpp'] is_static = get_option('default_library')=='static'