snapshot of https://github.com/ActivityWatch/aw-server-rust.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build / ubuntu-latest (push) Failing after 5s
Build / Android (push) Failing after 2s
Build / Code coverage (push) Failing after 2s
Lint / format (push) Failing after 3s
Lint / clippy (push) Failing after 3s
Build / macOS-latest (push) Has been cancelled
Build / windows-latest (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 00:19:17 +05:00
commit da934b693c
120 changed files with 26659 additions and 0 deletions

220
aw-datastore/src/export.rs Normal file
View File

@@ -0,0 +1,220 @@
use std::{collections::HashMap, io::Write};
use aw_models::Bucket;
use rusqlite::Connection;
use serde::{
ser::{Error, SerializeMap, SerializeSeq},
Serialize, Serializer,
};
use crate::DatastoreError;
struct EventRows<'a> {
conn: &'a Connection,
bucket: &'a Bucket,
}
impl Serialize for EventRows<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut stmt = self
.conn
.prepare_cached(
"SELECT id, starttime, endtime, data
FROM events INDEXED BY events_bucketrow_starttime_endtime_index
WHERE bucketrow = ?1 AND endtime >= 0 AND starttime <= ?2
ORDER BY starttime DESC, endtime ASC, id ASC",
)
.map_err(S::Error::custom)?;
let mut rows = stmt
.query(rusqlite::params![self.bucket.bid.unwrap(), i64::MAX])
.map_err(S::Error::custom)?;
let mut seq = serializer.serialize_seq(None)?;
while let Some(row) = rows.next().map_err(S::Error::custom)? {
// Match get_events' default range, clipping and corrupt-row policy.
match crate::datastore::parse_event_row(row, Some((0, i64::MAX))) {
Ok(event) => seq.serialize_element(&event)?,
Err(err) => warn!("Corrupt event in bucket {}: {}", self.bucket.id, err),
}
}
seq.end()
}
}
struct ExportBucket<'a> {
conn: &'a Connection,
bucket: &'a Bucket,
}
impl Serialize for ExportBucket<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
// Reuse Bucket's field names and serialization rules, replacing only
// events. This allocation contains bucket metadata, never event rows.
let metadata = serde_json::to_value(self.bucket).map_err(S::Error::custom)?;
let metadata = metadata
.as_object()
.ok_or_else(|| S::Error::custom("invalid bucket metadata"))?;
let mut map = serializer.serialize_map(Some(metadata.len()))?;
for (key, value) in metadata {
if key != "events" {
map.serialize_entry(key, value)?;
}
}
map.serialize_entry(
"events",
&EventRows {
conn: self.conn,
bucket: self.bucket,
},
)?;
map.end()
}
}
struct ExportBuckets<'a> {
conn: &'a Connection,
buckets: &'a HashMap<String, Bucket>,
selected: Option<&'a str>,
}
impl Serialize for ExportBuckets<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
for (id, bucket) in self.buckets {
if self.selected.is_none() || self.selected == Some(id.as_str()) {
map.serialize_entry(
id,
&ExportBucket {
conn: self.conn,
bucket,
},
)?;
}
}
map.end()
}
}
pub(crate) fn write_export(
conn: &Connection,
buckets: &HashMap<String, Bucket>,
selected: Option<&str>,
writer: impl Write,
) -> Result<(), DatastoreError> {
if let Some(id) = selected {
if !buckets.contains_key(id) {
return Err(DatastoreError::NoSuchBucket(id.to_owned()));
}
}
#[derive(Serialize)]
struct Export<'a> {
buckets: ExportBuckets<'a>,
}
serde_json::to_writer(
writer,
&Export {
buckets: ExportBuckets {
conn,
buckets,
selected,
},
},
)
.map_err(|err| DatastoreError::InternalError(format!("Failed to write export: {err}")))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DatastoreInstance;
use aw_models::{BucketMetadata, BucketsExport, Event, TryVec};
use chrono::{DateTime, Duration};
fn setup() -> (Connection, DatastoreInstance) {
let conn = Connection::open_in_memory().unwrap();
let mut ds = DatastoreInstance::new(&conn, true).unwrap();
for id in ["populated", "empty"] {
ds.create_bucket(
&conn,
Bucket {
bid: None,
id: id.into(),
_type: "test".into(),
client: "test".into(),
hostname: "host".into(),
created: None,
data: Default::default(),
metadata: BucketMetadata::default(),
events: None,
last_updated: None,
},
)
.unwrap();
}
let events = [(-10, 20), (5, 20), (5, 10), (5, 10), (30, 0)]
.into_iter()
.map(|(start, duration)| {
Event::new(
DateTime::from_timestamp(start, 0).unwrap(),
Duration::seconds(duration),
serde_json::from_value(
serde_json::json!({"text": "quotes \" and unicode ☀", "nested": [1, true]}),
)
.unwrap(),
)
})
.collect();
ds.insert_events(&conn, "populated", events).unwrap();
conn.execute("INSERT INTO events(bucketrow,starttime,endtime,data) VALUES(1,6000000000,7000000000,'invalid json')", []).unwrap();
(conn, ds)
}
#[test]
fn streamed_json_matches_materialized_exports() {
let (conn, mut ds) = setup();
for selected in [None, Some("populated"), Some("empty")] {
let mut buckets = ds.get_buckets();
buckets.retain(|id, _| selected.is_none() || selected == Some(id.as_str()));
for (id, bucket) in &mut buckets {
bucket.events = Some(TryVec::new(
ds.get_events(&conn, id, None, None, None).unwrap(),
));
}
let expected = serde_json::to_value(BucketsExport { buckets }).unwrap();
let mut output = Vec::new();
ds.write_export(&conn, selected, &mut output).unwrap();
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&output).unwrap(),
expected
);
}
}
#[test]
fn missing_bucket_and_writer_failures_propagate() {
let (conn, ds) = setup();
let mut output = Vec::new();
assert!(matches!(
ds.write_export(&conn, Some("missing"), &mut output),
Err(DatastoreError::NoSuchBucket(_))
));
assert!(output.is_empty());
struct FailingWriter(usize);
impl Write for FailingWriter {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
if self.0 == 0 {
return Err(std::io::Error::new(std::io::ErrorKind::Other, "disk full"));
}
let n = bytes.len().min(self.0);
self.0 -= n;
Ok(n)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
assert!(matches!(
ds.write_export(&conn, None, FailingWriter(500)),
Err(DatastoreError::InternalError(_))
));
}
}