mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00
add server support
This commit is contained in:
parent
c0403f3f38
commit
41a578ab2b
7 changed files with 267 additions and 6 deletions
1
integration-tests/.gitignore
vendored
1
integration-tests/.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
test-db
|
||||
test-sync-server
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <string.h>
|
||||
#include "unity.h"
|
||||
#include "taskchampion.h"
|
||||
|
@ -153,6 +154,48 @@ static void test_replica_task_creation(void) {
|
|||
tc_replica_free(rep);
|
||||
}
|
||||
|
||||
// When tc_replica_undo is passed NULL for undone_out, it still succeeds
|
||||
static void test_replica_sync_local(void) {
|
||||
TCReplica *rep = tc_replica_new_in_memory();
|
||||
TEST_ASSERT_NULL(tc_replica_error(rep));
|
||||
|
||||
mkdir("test-sync-server", 0755); // ignore error, if dir already exists
|
||||
|
||||
TCString *err = NULL;
|
||||
TCServer *server = tc_server_new_local(tc_string_borrow("test-sync-server"), &err);
|
||||
TEST_ASSERT_NOT_NULL(server);
|
||||
TEST_ASSERT_NULL(err);
|
||||
|
||||
int rv = tc_replica_sync(rep, server, false);
|
||||
TEST_ASSERT_EQUAL(TC_RESULT_OK, rv);
|
||||
TEST_ASSERT_NULL(tc_replica_error(rep));
|
||||
|
||||
tc_server_free(server);
|
||||
tc_replica_free(rep);
|
||||
|
||||
// test error handling
|
||||
server = tc_server_new_local(tc_string_borrow("/no/such/directory"), &err);
|
||||
TEST_ASSERT_NULL(server);
|
||||
TEST_ASSERT_NOT_NULL(err);
|
||||
tc_string_free(err);
|
||||
}
|
||||
|
||||
// When tc_replica_undo is passed NULL for undone_out, it still succeeds
|
||||
static void test_replica_remote_server(void) {
|
||||
TCString *err = NULL;
|
||||
TCServer *server = tc_server_new_remote(
|
||||
tc_string_borrow("tc.freecinc.com"),
|
||||
tc_uuid_new_v4(),
|
||||
tc_string_borrow("\xf0\x28\x8c\x28"), // NOTE: not utf-8
|
||||
&err);
|
||||
TEST_ASSERT_NOT_NULL(server);
|
||||
TEST_ASSERT_NULL(err);
|
||||
|
||||
// can't actually do anything with this server!
|
||||
|
||||
tc_server_free(server);
|
||||
}
|
||||
|
||||
// a replica with tasks in it returns an appropriate list of tasks and list of uuids
|
||||
static void test_replica_all_tasks(void) {
|
||||
TCReplica *rep = tc_replica_new_in_memory();
|
||||
|
@ -274,6 +317,8 @@ int replica_tests(void) {
|
|||
RUN_TEST(test_replica_working_set);
|
||||
RUN_TEST(test_replica_undo_empty_null_undone_out);
|
||||
RUN_TEST(test_replica_task_creation);
|
||||
RUN_TEST(test_replica_sync_local);
|
||||
RUN_TEST(test_replica_remote_server);
|
||||
RUN_TEST(test_replica_all_tasks);
|
||||
RUN_TEST(test_replica_task_import);
|
||||
RUN_TEST(test_replica_get_task_not_found);
|
||||
|
|
|
@ -14,6 +14,7 @@ pub mod atomic;
|
|||
pub mod kv;
|
||||
pub mod replica;
|
||||
pub mod result;
|
||||
pub mod server;
|
||||
pub mod status;
|
||||
pub mod string;
|
||||
pub mod task;
|
||||
|
@ -26,6 +27,7 @@ pub(crate) mod types {
|
|||
pub(crate) use crate::kv::{TCKVList, TCKV};
|
||||
pub(crate) use crate::replica::TCReplica;
|
||||
pub(crate) use crate::result::TCResult;
|
||||
pub(crate) use crate::server::TCServer;
|
||||
pub(crate) use crate::status::TCStatus;
|
||||
pub(crate) use crate::string::{TCString, TCStringList};
|
||||
pub(crate) use crate::task::{TCTask, TCTaskList};
|
||||
|
|
|
@ -100,8 +100,9 @@ pub unsafe extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica {
|
|||
unsafe { TCReplica::from(Replica::new(storage)).return_ptr() }
|
||||
}
|
||||
|
||||
/// Create a new TCReplica with an on-disk database having the given filename. On error, a string
|
||||
/// is written to the `error_out` parameter (if it is not NULL) and NULL is returned.
|
||||
/// Create a new TCReplica with an on-disk database having the given filename. On error, a string
|
||||
/// is written to the error_out parameter (if it is not NULL) and NULL is returned. The caller
|
||||
/// must free this string.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn tc_replica_new_on_disk(
|
||||
path: *mut TCString,
|
||||
|
@ -258,7 +259,31 @@ pub unsafe extern "C" fn tc_replica_import_task_with_uuid(
|
|||
)
|
||||
}
|
||||
|
||||
// TODO: tc_replica_sync
|
||||
/// Synchronize this replica with a server.
|
||||
///
|
||||
/// The `server` argument remains owned by the caller, and must be freed explicitly.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn tc_replica_sync(
|
||||
rep: *mut TCReplica,
|
||||
server: *mut TCServer,
|
||||
avoid_snapshots: bool,
|
||||
) -> TCResult {
|
||||
wrap(
|
||||
rep,
|
||||
|rep| {
|
||||
debug_assert!(!server.is_null());
|
||||
// SAFETY:
|
||||
// - server is not NULL
|
||||
// - *server is a valid TCServer (promised by caller)
|
||||
// - server is valid for the lifetime of tc_replica_sync (not threadsafe)
|
||||
// - server will not be accessed simultaneously (not threadsafe)
|
||||
let server = unsafe { TCServer::from_ptr_arg_ref_mut(server) };
|
||||
rep.sync(server.as_mut(), avoid_snapshots)?;
|
||||
Ok(TCResult::Ok)
|
||||
},
|
||||
TCResult::Error,
|
||||
)
|
||||
}
|
||||
|
||||
/// Undo local operations until the most recent UndoPoint.
|
||||
///
|
||||
|
|
139
lib/src/server.rs
Normal file
139
lib/src/server.rs
Normal file
|
@ -0,0 +1,139 @@
|
|||
use crate::traits::*;
|
||||
use crate::types::*;
|
||||
use crate::util::err_to_tcstring;
|
||||
use taskchampion::{Server, ServerConfig};
|
||||
|
||||
/// TCServer represents an interface to a sync server. Aside from new and free, a server
|
||||
/// has no C-accessible API, but is designed to be passed to `tc_replica_sync`.
|
||||
///
|
||||
/// ## Safety
|
||||
///
|
||||
/// TCServer are not threadsafe, and must not be used with multiple replicas simultaneously.
|
||||
pub struct TCServer(Box<dyn Server>);
|
||||
|
||||
impl PassByPointer for TCServer {}
|
||||
|
||||
impl From<Box<dyn Server>> for TCServer {
|
||||
fn from(server: Box<dyn Server>) -> TCServer {
|
||||
TCServer(server)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<Box<dyn Server>> for TCServer {
|
||||
fn as_mut(&mut self) -> &mut Box<dyn Server> {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility function to allow using `?` notation to return an error value.
|
||||
fn wrap<T, F>(f: F, error_out: *mut *mut TCString, err_value: T) -> T
|
||||
where
|
||||
F: FnOnce() -> anyhow::Result<T>,
|
||||
{
|
||||
match f() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
if !error_out.is_null() {
|
||||
// SAFETY:
|
||||
// - error_out is not NULL (checked)
|
||||
// - ..and points to a valid pointer (promised by caller)
|
||||
// - caller will free this string (promised by caller)
|
||||
unsafe {
|
||||
*error_out = err_to_tcstring(e).return_ptr();
|
||||
}
|
||||
}
|
||||
err_value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new TCServer that operates locally (on-disk). See the TaskChampion docs for the
|
||||
/// description of the arguments.
|
||||
///
|
||||
/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is
|
||||
/// returned. The caller must free this string.
|
||||
///
|
||||
/// The server must be freed after it is used - tc_replica_sync does not automatically free it.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn tc_server_new_local(
|
||||
server_dir: *mut TCString,
|
||||
error_out: *mut *mut TCString,
|
||||
) -> *mut TCServer {
|
||||
wrap(
|
||||
|| {
|
||||
// SAFETY: see TCString docstring
|
||||
let server_dir = unsafe { TCString::take_from_ptr_arg(server_dir) };
|
||||
let server_config = ServerConfig::Local {
|
||||
server_dir: server_dir.to_path_buf(),
|
||||
};
|
||||
let server = server_config.into_server()?;
|
||||
// SAFETY: caller promises to free this server.
|
||||
Ok(unsafe { TCServer::return_ptr(server.into()) })
|
||||
},
|
||||
error_out,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a new TCServer that connects to a remote server. See the TaskChampion docs for the
|
||||
/// description of the arguments.
|
||||
///
|
||||
/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is
|
||||
/// returned. The caller must free this string.
|
||||
///
|
||||
/// The server must be freed after it is used - tc_replica_sync does not automatically free it.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn tc_server_new_remote(
|
||||
origin: *mut TCString,
|
||||
client_key: TCUuid,
|
||||
encryption_secret: *mut TCString,
|
||||
error_out: *mut *mut TCString,
|
||||
) -> *mut TCServer {
|
||||
wrap(
|
||||
|| {
|
||||
debug_assert!(!origin.is_null());
|
||||
debug_assert!(!encryption_secret.is_null());
|
||||
// SAFETY:
|
||||
// - origin is not NULL
|
||||
// - origin is valid (promised by caller)
|
||||
// - origin ownership is transferred to this function
|
||||
let origin = unsafe { TCString::take_from_ptr_arg(origin) }.into_string()?;
|
||||
|
||||
// SAFETY:
|
||||
// - client_key is a valid Uuid (any 8-byte sequence counts)
|
||||
|
||||
let client_key = unsafe { TCUuid::val_from_arg(client_key) };
|
||||
// SAFETY:
|
||||
// - encryption_secret is not NULL
|
||||
// - encryption_secret is valid (promised by caller)
|
||||
// - encryption_secret ownership is transferred to this function
|
||||
let encryption_secret = unsafe { TCString::take_from_ptr_arg(encryption_secret) }
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
|
||||
let server_config = ServerConfig::Remote {
|
||||
origin,
|
||||
client_key,
|
||||
encryption_secret,
|
||||
};
|
||||
let server = server_config.into_server()?;
|
||||
// SAFETY: caller promises to free this server.
|
||||
Ok(unsafe { TCServer::return_ptr(server.into()) })
|
||||
},
|
||||
error_out,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Free a server. The server may not be used after this function returns and must not be freed
|
||||
/// more than once.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn tc_server_free(server: *mut TCServer) {
|
||||
debug_assert!(!server.is_null());
|
||||
// SAFETY:
|
||||
// - server is not NULL
|
||||
// - server came from tc_server_new_.., which used return_ptr
|
||||
// - server will not be used after (promised by caller)
|
||||
let server = unsafe { TCServer::take_from_ptr_arg(server) };
|
||||
drop(server);
|
||||
}
|
|
@ -82,7 +82,7 @@ impl<'a> TCString<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn as_bytes(&self) -> &[u8] {
|
||||
pub(crate) fn as_bytes(&self) -> &[u8] {
|
||||
match self {
|
||||
TCString::CString(cstring) => cstring.as_bytes(),
|
||||
TCString::CStr(cstr) => cstr.to_bytes(),
|
||||
|
|
|
@ -63,6 +63,16 @@ typedef enum TCStatus {
|
|||
*/
|
||||
typedef struct TCReplica TCReplica;
|
||||
|
||||
/**
|
||||
* TCServer represents an interface to a sync server. Aside from new and free, a server
|
||||
* has no C-accessible API, but is designed to be passed to `tc_replica_sync`.
|
||||
*
|
||||
* ## Safety
|
||||
*
|
||||
* TCServer are not threadsafe, and must not be used with multiple replicas simultaneously.
|
||||
*/
|
||||
typedef struct TCServer TCServer;
|
||||
|
||||
/**
|
||||
* TCString supports passing strings into and out of the TaskChampion API.
|
||||
*
|
||||
|
@ -339,8 +349,9 @@ void tc_kv_list_free(struct TCKVList *tckvs);
|
|||
struct TCReplica *tc_replica_new_in_memory(void);
|
||||
|
||||
/**
|
||||
* Create a new TCReplica with an on-disk database having the given filename. On error, a string
|
||||
* is written to the `error_out` parameter (if it is not NULL) and NULL is returned.
|
||||
* Create a new TCReplica with an on-disk database having the given filename. On error, a string
|
||||
* is written to the error_out parameter (if it is not NULL) and NULL is returned. The caller
|
||||
* must free this string.
|
||||
*/
|
||||
struct TCReplica *tc_replica_new_on_disk(struct TCString *path, struct TCString **error_out);
|
||||
|
||||
|
@ -389,6 +400,13 @@ struct TCTask *tc_replica_new_task(struct TCReplica *rep,
|
|||
*/
|
||||
struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TCUuid tcuuid);
|
||||
|
||||
/**
|
||||
* Synchronize this replica with a server.
|
||||
*
|
||||
* The `server` argument remains owned by the caller, and must be freed explicitly.
|
||||
*/
|
||||
TCResult tc_replica_sync(struct TCReplica *rep, struct TCServer *server, bool avoid_snapshots);
|
||||
|
||||
/**
|
||||
* Undo local operations until the most recent UndoPoint.
|
||||
*
|
||||
|
@ -425,6 +443,37 @@ struct TCString *tc_replica_error(struct TCReplica *rep);
|
|||
*/
|
||||
void tc_replica_free(struct TCReplica *rep);
|
||||
|
||||
/**
|
||||
* Create a new TCServer that operates locally (on-disk). See the TaskChampion docs for the
|
||||
* description of the arguments.
|
||||
*
|
||||
* On error, a string is written to the error_out parameter (if it is not NULL) and NULL is
|
||||
* returned. The caller must free this string.
|
||||
*
|
||||
* The server must be freed after it is used - tc_replica_sync does not automatically free it.
|
||||
*/
|
||||
struct TCServer *tc_server_new_local(struct TCString *server_dir, struct TCString **error_out);
|
||||
|
||||
/**
|
||||
* Create a new TCServer that connects to a remote server. See the TaskChampion docs for the
|
||||
* description of the arguments.
|
||||
*
|
||||
* On error, a string is written to the error_out parameter (if it is not NULL) and NULL is
|
||||
* returned. The caller must free this string.
|
||||
*
|
||||
* The server must be freed after it is used - tc_replica_sync does not automatically free it.
|
||||
*/
|
||||
struct TCServer *tc_server_new_remote(struct TCString *origin,
|
||||
struct TCUuid client_key,
|
||||
struct TCString *encryption_secret,
|
||||
struct TCString **error_out);
|
||||
|
||||
/**
|
||||
* Free a server. The server may not be used after this function returns and must not be freed
|
||||
* more than once.
|
||||
*/
|
||||
void tc_server_free(struct TCServer *server);
|
||||
|
||||
/**
|
||||
* Create a new TCString referencing the given C string. The C string must remain valid and
|
||||
* unchanged until after the TCString is freed. It's typically easiest to ensure this by using a
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue