From 5276a9f4bd206561cb41ba3a2fb8602123c68ee8 Mon Sep 17 00:00:00 2001 From: Thomas Lauf Date: Sat, 21 Jun 2025 13:34:18 +0200 Subject: [PATCH] Add import command The import command lets the user import time tracking data as produced by the export command, i.e. a JSON array of interval objects. Data can be either provided as one or more files, or via stdin. The latter enables synchronization between remote machines via ssh: timew export | ssh 'timew import' Signed-off-by: Thomas Lauf --- ChangeLog | 2 + doc/man1/timew-import.1.adoc | 57 ++++++++++++ src/commands/CMakeLists.txt | 1 + src/commands/CmdImport.cpp | 167 +++++++++++++++++++++++++++++++++++ src/commands/commands.h | 1 + src/init.cpp | 2 + 6 files changed, 230 insertions(+) create mode 100644 doc/man1/timew-import.1.adoc create mode 100644 src/commands/CmdImport.cpp diff --git a/ChangeLog b/ChangeLog index 9e0bcdaa..8be430c1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,5 @@ +- #676 Add import command + (thanks to Shaun Ruffel) - #677 Extension names starting with 'timew' cause problems (thanks to ftambara) - #661 Make display of ids and annotations the default in summary report for new users diff --git a/doc/man1/timew-import.1.adoc b/doc/man1/timew-import.1.adoc new file mode 100644 index 00000000..b15aab9d --- /dev/null +++ b/doc/man1/timew-import.1.adoc @@ -0,0 +1,57 @@ += timew-import(1) + +== NAME +timew-import - import time-tracking data from files + +== SYNOPSIS +[verse] +*timew import* [__**...**] + +== DESCRIPTION +Import tracked time from `file`. +If no files are specified, the command will read from standard input. + +The data to import has to be in JSON format, as exported by **timew-export**(1), i.e. a single array with interval objects. + +When importing, the intervals are checked for overlaps with existing intervals. +If an overlap is found, the import will abort at the first overlap and no more intervals are imported, unless the `:adjust` hint is specified. + +In general, it is recommended to create a backup of your data before importing. + +== HINTS + +**:adjust**:: +When given, the imported interval will overwrite any existing intervals that it overlaps with. + +== EXAMPLES + +*Import intervals from a file*:: ++ +Import intervals from a single file: ++ +[source] +---- +timew import intervals.json +---- ++ +Any file path that does not start with a `/` is interpreted as relative to the current working directory. + +*Import intervals from multiple files*:: ++ +One can also use shell wildcards and absolute paths: ++ +[source] +---- +timew import /path/to/intervals/*.json +---- + +*Import intervals from standard input*:: ++ +Import intervals from stdin: ++ +[source] +---- +timew export | ssh 'timew import' +---- +This is especially useful for synchronizing intervals between different machines. ++ diff --git a/src/commands/CMakeLists.txt b/src/commands/CMakeLists.txt index 9f0ced41..f0a752bb 100644 --- a/src/commands/CMakeLists.txt +++ b/src/commands/CMakeLists.txt @@ -30,6 +30,7 @@ set (commands_SRCS CmdAnnotate.cpp CmdGaps.cpp CmdGet.cpp CmdHelp.cpp + CmdImport.cpp CmdJoin.cpp CmdLengthen.cpp CmdModify.cpp diff --git a/src/commands/CmdImport.cpp b/src/commands/CmdImport.cpp new file mode 100644 index 00000000..39e78ebb --- /dev/null +++ b/src/commands/CmdImport.cpp @@ -0,0 +1,167 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2025, Gothenburg Bit Factory. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// https://www.opensource.org/licenses/mit-license.php +// +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +std::vector parse_content (const std::string& content) +{ + const std::unique_ptr json(json::parse (content)); + + if (content.empty() || (json == nullptr)) + { + throw std::string ("Contents invalid."); + } + + if (json->type () != json::j_array) + { + throw std::string ("Expected JSON array for import data."); + } + + std::vector intervals; + + for (const auto item: dynamic_cast (json.get ())->_data) + { + Interval new_interval = IntervalFactory::fromJson (item->dump ()); + intervals.push_back (new_interval); + } + + return intervals; +} + +//////////////////////////////////////////////////////////////////////////////// +std::string read_input () +{ + std::string content; + std::string line; + + while (std::getline (std::cin, line)) + { + content += line; + } + + return content; +} + +//////////////////////////////////////////////////////////////////////////////// +std::vector import_file (const std::string& file_name) +{ + Path file_path; + + if (file_name.empty ()) + { + throw format ("Attempted to import from empty file name!", file_name); + } + + if (file_name.at (0) == '/') + { + file_path = file_name; + } + else + { + file_path = Directory::cwd () + "/" + file_name; + } + + std::string content; + + if (const bool exists = file_path.exists (); exists && File::read (file_path, content)) + { + return parse_content (content); + } + + throw format ("File {1} does not exist or cannot be read!", file_name); +} + +void import_intervals ( + const CLI& cli, + const Rules& rules, + Database& database, + Journal& journal, + const bool verbose, + std::vector& intervals) +{ + journal.startTransaction (); + for (auto& interval: intervals) + { + // Add each interval to the database + if (validate (cli, rules, database, interval)) + { + database.addInterval (interval, verbose); + database.commit (); + } + } + journal.endTransaction (); +} + +//////////////////////////////////////////////////////////////////////////////// +int CmdImport ( + CLI& cli, + Rules& rules, + Database &database, + Journal &journal) +{ + const bool verbose = rules.getBoolean ("verbose"); + + if (const auto fileNames = cli.getWords (); fileNames.empty ()) + { + const auto content = read_input (); + auto intervals = parse_content (content); + + import_intervals (cli, rules, database, journal, verbose, intervals); + + if (verbose) + { + std::cout << "Imported " << intervals.size () << " interval(s)." << std::endl; + } + + } + else + { + for (const auto& fileName: fileNames) + { + try + { + auto intervals = import_file (fileName); + + import_intervals (cli, rules, database, journal, verbose, intervals); + + if (verbose) + { + std::cout << "Imported " << intervals.size () << " interval(s) from '" << fileName << "'." << std::endl; + } + } catch (const std::string& error) + { + throw format ("Error importing '{1}': {2}", fileName, error); + } + } + } + return 0; +} diff --git a/src/commands/commands.h b/src/commands/commands.h index 8b156d17..4f9131de 100644 --- a/src/commands/commands.h +++ b/src/commands/commands.h @@ -47,6 +47,7 @@ int CmdGaps (CLI&, Rules&, Database& ); int CmdGet (CLI&, Rules&, Database& ); int CmdHelpUsage ( const Extensions&); int CmdHelp (CLI&, const Extensions&); +int CmdImport (CLI&, Rules&, Database&, Journal& ); int CmdJoin (CLI&, Rules&, Database&, Journal& ); int CmdLengthen (CLI&, Rules&, Database&, Journal& ); int CmdModify (CLI&, Rules&, Database&, Journal& ); diff --git a/src/init.cpp b/src/init.cpp index f8645ccb..1ea86956 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -63,6 +63,7 @@ void initializeEntities (CLI& cli) cli.entity ("command", "help"); cli.entity ("command", "--help"); cli.entity ("command", "-h"); + cli.entity ("command", "import"); cli.entity ("command", "join"); cli.entity ("command", "lengthen"); cli.entity ("command", "modify"); @@ -235,6 +236,7 @@ int dispatchCommand ( else if (command == "help" || command == "--help" || command == "-h") status = CmdHelp (cli, extensions); + else if (command == "import") status = CmdImport (cli, rules, database, journal ); else if (command == "join") status = CmdJoin (cli, rules, database, journal ); else if (command == "lengthen") status = CmdLengthen (cli, rules, database, journal ); else if (command == "modify") status = CmdModify (cli, rules, database, journal );