mirror of
https://github.com/ankitects/anki.git
synced 2025-11-08 13:47:13 -05:00
This PR replaces the existing Python-driven sync server with a new one in Rust. The new server supports both collection and media syncing, and is compatible with both the new protocol mentioned below, and older clients. A setting has been added to the preferences screen to point Anki to a local server, and a similar setting is likely to come to AnkiMobile soon. Documentation is available here: <https://docs.ankiweb.net/sync-server.html> In addition to the new server and refactoring, this PR also makes changes to the sync protocol. The existing sync protocol places payloads and metadata inside a multipart POST body, which causes a few headaches: - Legacy clients build the request in a non-deterministic order, meaning the entire request needs to be scanned to extract the metadata. - Reqwest's multipart API directly writes the multipart body, without exposing the resulting stream to us, making it harder to track the progress of the transfer. We've been relying on a patched version of reqwest for timeouts, which is a pain to keep up to date. To address these issues, the metadata is now sent in a HTTP header, with the data payload sent directly in the body. Instead of the slower gzip, we now use zstd. The old timeout handling code has been replaced with a new implementation that wraps the request and response body streams to track progress, allowing us to drop the git dependencies for reqwest, hyper-timeout and tokio-io-timeout. The main other change to the protocol is that one-way syncs no longer need to downgrade the collection to schema 11 prior to sending.
90 lines
2.5 KiB
Rust
90 lines
2.5 KiB
Rust
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
use std::marker::PhantomData;
|
|
|
|
use axum::{
|
|
body::StreamBody,
|
|
headers::HeaderName,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use serde::{de::DeserializeOwned, Serialize};
|
|
|
|
use crate::{
|
|
prelude::*,
|
|
sync::{
|
|
collection::upload::UploadResponse,
|
|
error::{HttpResult, OrHttpErr},
|
|
request::header_and_stream::encode_zstd_body,
|
|
version::SyncVersion,
|
|
},
|
|
};
|
|
|
|
pub static ORIGINAL_SIZE: HeaderName = HeaderName::from_static("anki-original-size");
|
|
|
|
/// Stores the data returned from a sync request, and the type
|
|
/// it represents. Given a SyncResponse<Foo>, you can get a Foo
|
|
/// struct via .json(), except for uploads/downloads.
|
|
#[derive(Debug)]
|
|
pub struct SyncResponse<T> {
|
|
pub data: Vec<u8>,
|
|
json_output_type: PhantomData<T>,
|
|
}
|
|
|
|
impl<T> SyncResponse<T> {
|
|
pub fn from_vec(data: Vec<u8>) -> SyncResponse<T> {
|
|
SyncResponse {
|
|
data,
|
|
json_output_type: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn make_response(self, sync_version: SyncVersion) -> Response {
|
|
if sync_version.is_zstd() {
|
|
let header = (&ORIGINAL_SIZE, self.data.len().to_string());
|
|
let body = StreamBody::new(encode_zstd_body(self.data));
|
|
([header], body).into_response()
|
|
} else {
|
|
self.data.into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SyncResponse<UploadResponse> {
|
|
// Unfortunately the sync protocol sends this as a bare string
|
|
// instead of JSON.
|
|
pub fn upload_response(&self) -> UploadResponse {
|
|
let resp = String::from_utf8_lossy(&self.data);
|
|
match resp.as_ref() {
|
|
"OK" => UploadResponse::Ok,
|
|
other => UploadResponse::Err(other.into()),
|
|
}
|
|
}
|
|
|
|
pub fn from_upload_response(resp: UploadResponse) -> Self {
|
|
let text = match resp {
|
|
UploadResponse::Ok => "OK".into(),
|
|
UploadResponse::Err(other) => other,
|
|
};
|
|
SyncResponse::from_vec(text.into_bytes())
|
|
}
|
|
}
|
|
|
|
impl<T> SyncResponse<T>
|
|
where
|
|
T: Serialize,
|
|
{
|
|
pub fn try_from_obj(obj: T) -> HttpResult<SyncResponse<T>> {
|
|
let data = serde_json::to_vec(&obj).or_internal_err("couldn't serialize object")?;
|
|
Ok(SyncResponse::from_vec(data))
|
|
}
|
|
}
|
|
|
|
impl<T> SyncResponse<T>
|
|
where
|
|
T: DeserializeOwned,
|
|
{
|
|
pub fn json(&self) -> Result<T> {
|
|
serde_json::from_slice(&self.data).map_err(Into::into)
|
|
}
|
|
}
|