Refactor to new structure
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing

This commit is contained in:
Armin Friedl 2022-06-25 19:12:12 +02:00
parent 7141e67e14
commit e6a6e9268e
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
13 changed files with 334 additions and 179 deletions

View file

@ -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

View file

@ -1,6 +1,5 @@
#include "Archiver.hpp"
#include <spdlog/common.h>
#include <spdlog/spdlog.h>
#include <filesystem>
@ -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<Archiver> make_archiver(const string& archive_name) {
archive_name};
};
}
} // namespace xwim

View file

@ -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<Archiver> make_archiver(const std::string& archive_name);

190
src/UserIntent.cpp Normal file
View file

@ -0,0 +1,190 @@
#include "UserIntent.hpp"
#include <algorithm>
#include <filesystem>
#include <spdlog/spdlog.h>
#include "Archiver.hpp"
namespace xwim {
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...");
}
// compression intent explicitly specified
if (userOpt.wants_compress()) {
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");
}
return make_unique<CompressManyIntent>(
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>(
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");
}
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> 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> 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);
}
};
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);
};
} // namespace xwim

89
src/UserIntent.hpp Normal file
View file

@ -0,0 +1,89 @@
#pragma once
#include <optional>
#include <set>
#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<UserIntent> 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<path> archives;
optional<path> out;
public:
ExtractIntent(set<path> archives, optional<path> 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 <path> out;
public:
CompressSingleIntent(path in, optional <path> 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<path> in_paths;
path out;
public:
CompressManyIntent(set<path> in_paths, path out): UserIntent(), in_paths(in_paths), out(out) {};
~CompressManyIntent() = default;
void execute();
};
} // namespace xwim

View file

@ -34,16 +34,17 @@ UserOpt::UserOpt(int argc, char** argv) {
{"o", "out", "Out <file-or-path>", false, fs::path{}, "A path on the filesystem", cmd};
TCLAP::UnlabeledMultiArg<fs::path> 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<fs::path>{arg_paths.getValue().begin(), arg_paths.getValue().end()};

View file

@ -10,13 +10,21 @@ using namespace std;
namespace fs = std::filesystem;
struct UserOpt {
bool compress;
bool extract;
optional<bool> compress;
optional<bool> extract;
bool interactive;
std::optional<fs::path> out;
std::set<fs::path> 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

View file

@ -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:

View file

@ -1,122 +0,0 @@
#include "XwimIntent.hpp"
#include <spdlog/spdlog.h>
#include <tclap/ArgException.h>
#include <tclap/CmdLine.h>
#include <tclap/StdOutput.h>
#include <tclap/SwitchArg.h>
#include <tclap/UnlabeledMultiArg.h>
#include <tclap/ValueArg.h>
#include <algorithm>
#include <exception>
#include <filesystem>
#include <iostream>
#include <optional>
#include "Archiver.hpp"
template <>
struct::TCLAP::ArgTraits<std::filesystem::path> {
// `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 <files>", cmd, false};
TCLAP::SwitchArg arg_extract
{"x", "extract", "Extract <file>", cmd, false};
TCLAP::SwitchArg arg_noninteractive
{"i", "non-interactive", "Non-interactive, fail on ambiguity", cmd, false};
TCLAP::ValueArg<path> arg_outfile
{"o", "out", "Out <file-or-path>", false, path{}, "A path on the filesystem", cmd};
TCLAP::UnlabeledMultiArg<path> 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<path> UserOpt::paths_intent() {
}
} // namespace xwim

View file

@ -1,38 +0,0 @@
#pragma once
#include <optional>
#include <set>
#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<path> paths;
};
class UserOpt {
private:
bool compress = true;
bool extract = false;
bool interactive = true;
optional<path> out = nullopt;
vector<path> paths = std::vector<path>{};
Action action_intent();
path out_intent();
set<path> paths_intent();
public:
void parse_config(path config);
void parse_args(int argc, char** argv);
UserIntent guess_intent();
};
} // namespace xwim

View file

@ -27,8 +27,9 @@ void LibArchiver::compress(set<fs::path> ins, fs::path archive_out) {
// complete type. `archive` is forward declared only.
shared_ptr<archive> writer;
writer = shared_ptr<archive>(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<archive> reader;

View file

@ -7,6 +7,7 @@
#include <optional>
#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<UserIntent> user_intent = make_intent(user_opt);
user_intent->execute();
} catch(XwimError& e) {
spdlog::error(e.what());
}
}

View file

@ -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'