Skip to content

Commit

Permalink
Add support for cloud sync, specifically GCP
Browse files Browse the repository at this point in the history
This adds generic support for sync to cloud services, with specific
spuport for GCP. Adding others -- so long as they support a
compare-and-set operation -- should be comparatively straightforward.

The cloud support includes cleanup of unnecessary data, and should keep
total space usage roughly proportional to the number of tasks.
  • Loading branch information
djmitche committed Dec 24, 2023
1 parent 1380d79 commit c5dae08
Show file tree
Hide file tree
Showing 31 changed files with 3,901 additions and 358 deletions.
733 changes: 682 additions & 51 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,21 @@ env_logger = "^0.10.0"
ffizz-header = "0.5"
flate2 = "1"
futures = "^0.3.25"
google-cloud-storage = { version = "0.15.0", default-features = false, features = ["rustls-tls", "auth"] }
lazy_static = "1"
libc = "0.2.136"
log = "^0.4.17"
pretty_assertions = "1"
proptest = "^1.4.0"
ring = "0.16"
ring = "0.17"
rstest = "0.17"
rusqlite = { version = "0.29", features = ["bundled"] }
serde_json = "^1.0"
serde = { version = "^1.0.147", features = ["derive"] }
strum = "0.25"
strum_macros = "0.25"
tempfile = "3"
tokio = { version = "1", features = ["rt-multi-thread"] }
thiserror = "1.0"
ureq = "^2.9.0"
ureq = { version = "^2.8.0", features = ["tls"] }
uuid = { version = "^1.6.0", features = ["serde", "v4"] }
47 changes: 36 additions & 11 deletions doc/man/task-sync.5.in
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,35 @@ NOTE: A side-effect of synchronization is that once changes have been
synchronized, they cannot be undone. This means that each time synchronization
is run, it is no longer possible to undo previous operations.

.SH MANAGING SYNCHRONIZATION

.SS Adding a Replica

To add a new replica, configure a new, empty replica identically to
the existing replica, and run `task sync`.

.SS When to Synchronize

Taskwarrior can perform a sync operation at every garbage collection (gc) run.
This is the default, and is appropriate for local synchronization.

For synchronization to a server, a better solution is to run

$ task sync

periodically, such as via
.BR cron (8) .

.SH CONFIGURATION

Taskwarrior provides several options for synchronizing your tasks:

- To a server specifically designed to handle Taskwarrior data.
- To a cloud service such as GCP or AWS.
- To a local, on-disk file.

.SS Sync Server

To synchronize your tasks to a sync server, you will need the following
information from the server administrator:

Expand All @@ -43,22 +70,20 @@ Configure Taskwarrior with these details:
$ task config sync.server.client_id <client_id>
$ task config sync.server.encryption_secret <encryption_secret>

.SS Adding a Replica

To add a new replica, configure a new, empty replica identically to
the existing replica, and run `task sync`.
.SS Google Cloud Platform

.SS When to Synchronize
To synchronize your tasks to GCP, use the GCP Console to create a new project,
and within that project a new Cloud Storage bucket. The default settings for
the bucket are adequete.

Taskwarrior can perform a sync operation at every garbage collection (gc) run.
This is the default, and is appropriate for local synchronization.
Authenticate to the project with

For synchronization to a server, a better solution is to run
gcloud config set project $PROJECT_NAME
gcloud auth application-default login

$ task sync
Then configure Taskwarrior with:

periodically, such as via
.BR cron (8) .
$ task config sync.gcp.bucket <bucket-name>

.SS Local Synchronization

Expand Down
1 change: 1 addition & 0 deletions src/Context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ std::string configurationDefaults =
"#sync.server.encryption_secret # Encryption secret for sync to a server\n"
"#sync.server.origin # Origin of the sync server\n"
"#sync.local.server_dir # Directory for local sync\n"
"#sync.gcp.bucket # Bucket for sync to GCP\n"
"\n"
"# Aliases - alternate names for commands\n"
"alias.rm=delete # Alias for the delete command\n"
Expand Down
1 change: 1 addition & 0 deletions src/commands/CmdShow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ int CmdShow::execute (std::string& output)
" sugar"
" summary.all.projects"
" sync.local.server_dir"
" sync.gcp.bucket"
" sync.server.client_id"
" sync.server.encryption_secret"
" sync.server.origin"
Expand Down
29 changes: 22 additions & 7 deletions src/commands/CmdSync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,32 @@ int CmdSync::execute (std::string& output)

// If no server is set up, quit.
std::string origin = Context::getContext ().config.get ("sync.server.origin");
std::string client_id = Context::getContext ().config.get ("sync.server.client_id");
std::string encryption_secret = Context::getContext ().config.get ("sync.server.encryption_secret");
std::string server_dir = Context::getContext ().config.get ("sync.local.server_dir");
std::string gcp_bucket = Context::getContext ().config.get ("sync.gcp.bucket");
if (server_dir != "") {
server = tc::Server (server_dir);
server = tc::Server::new_local (server_dir);
server_ident = server_dir;
} else if (origin != "" && client_id != "" && encryption_secret != "") {
server = tc::Server (origin, client_id, encryption_secret);
server_ident = origin;
} else if (gcp_bucket != "") {
std::string encryption_secret = Context::getContext ().config.get ("sync.gcp.encryption_secret");
if (encryption_secret == "") {
throw std::string ("sync.gcp.encryption_secret is required");
}
server = tc::Server::new_gcp (gcp_bucket, encryption_secret);
std::ostringstream os;
os << "GCP bucket " << gcp_bucket;
server_ident = os.str();
} else if (origin != "") {
std::string client_id = Context::getContext ().config.get ("sync.server.client_id");
std::string encryption_secret = Context::getContext ().config.get ("sync.server.encryption_secret");
if (client_id == "" || encryption_secret == "") {
throw std::string ("sync.server.client_id and encryption_secret are required");
}
server = tc::Server::new_sync (origin, client_id, encryption_secret);
std::ostringstream os;
os << "Sync server at " << origin;
server_ident = os.str();
} else {
throw std::string ("Neither sync.server nor sync.local are configured.");
throw std::string ("No sync.* settings are configured.");
}

std::stringstream out;
Expand Down
38 changes: 29 additions & 9 deletions src/tc/Server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
using namespace tc::ffi;

////////////////////////////////////////////////////////////////////////////////
tc::Server::Server (const std::string &server_dir)
tc::Server
tc::Server::new_local (const std::string &server_dir)
{
TCString tc_server_dir = tc_string_borrow (server_dir.c_str ());
TCString error;
Expand All @@ -43,18 +44,17 @@ tc::Server::Server (const std::string &server_dir)
tc_string_free (&error);
throw errmsg;
}
inner = unique_tcserver_ptr (
return Server (unique_tcserver_ptr (
tcserver,
[](TCServer* rep) { tc_server_free (rep); });
[](TCServer* rep) { tc_server_free (rep); }));
}

////////////////////////////////////////////////////////////////////////////////
tc::Server::Server (const std::string &origin, const std::string &client_id, const std::string &encryption_secret)
tc::Server
tc::Server::new_sync (const std::string &origin, const std::string &client_id, const std::string &encryption_secret)
{
TCString tc_origin = tc_string_borrow (origin.c_str ());

TCString tc_client_id = tc_string_borrow (client_id.c_str ());

TCString tc_encryption_secret = tc_string_borrow (encryption_secret.c_str ());

TCUuid tc_client_uuid;
Expand All @@ -65,16 +65,36 @@ tc::Server::Server (const std::string &origin, const std::string &client_id, con
}

TCString error;
auto tcserver = tc_server_new_remote (tc_origin, tc_client_uuid, tc_encryption_secret, &error);
auto tcserver = tc_server_new_sync (tc_origin, tc_client_uuid, tc_encryption_secret, &error);
if (!tcserver) {
auto errmsg = format ("Could not configure connection to server at {1}: {2}",
origin, tc_string_content (&error));
tc_string_free (&error);
throw errmsg;
}
inner = unique_tcserver_ptr (
return Server (unique_tcserver_ptr (
tcserver,
[](TCServer* rep) { tc_server_free (rep); });
[](TCServer* rep) { tc_server_free (rep); }));
}

////////////////////////////////////////////////////////////////////////////////
tc::Server
tc::Server::new_gcp (const std::string &bucket, const std::string &encryption_secret)
{
TCString tc_bucket = tc_string_borrow (bucket.c_str ());
TCString tc_encryption_secret = tc_string_borrow (encryption_secret.c_str ());

TCString error;
auto tcserver = tc_server_new_gcp (tc_bucket, tc_encryption_secret, &error);
if (!tcserver) {
auto errmsg = format ("Could not configure connection to GCP bucket {1}: {2}",
bucket, tc_string_content (&error));
tc_string_free (&error);
throw errmsg;
}
return Server (unique_tcserver_ptr (
tcserver,
[](TCServer* rep) { tc_server_free (rep); }));
}

////////////////////////////////////////////////////////////////////////////////
Expand Down
13 changes: 9 additions & 4 deletions src/tc/Server.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,21 @@ namespace tc {

// Server wraps the TCServer type, managing its memory, errors, and so on.
//
// Except as noted, method names match the suffix to `tc_replica_..`.
// Except as noted, method names match the suffix to `tc_server_..`.
class Server
{
public:
// Construct a null server
Server () = default;

// Construct a local server (tc_server_new_local).
Server (const std::string& server_dir);
static Server new_local (const std::string& server_dir);

// Construct a remote server (tc_server_new_remote).
Server (const std::string &origin, const std::string &client_id, const std::string &encryption_secret);
// Construct a remote server (tc_server_new_sync).
static Server new_sync (const std::string &origin, const std::string &client_id, const std::string &encryption_secret);

// Construct a GCP server (tc_server_new_gcp).
static Server new_gcp (const std::string &bucket, const std::string &encryption_secret);

// This object "owns" inner, so copy is not allowed.
Server (const Server &) = delete;
Expand All @@ -65,6 +68,8 @@ namespace tc {
Server &operator=(Server &&) noexcept;

protected:
Server (unique_tcserver_ptr inner) : inner(std::move(inner)) {};

unique_tcserver_ptr inner;

// Replica accesses the inner pointer to call tc_replica_sync
Expand Down
Loading

0 comments on commit c5dae08

Please sign in to comment.