AGPL-3.0-only, a source link in the app, and everything on dev since #5 #6
@@ -15,6 +15,7 @@ pub mod store;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use chrono::{DateTime, SecondsFormat, Utc};
|
||||
use rusqlite::Connection;
|
||||
|
||||
/// The store's file inside a client's data directory. The layout is the same on
|
||||
@@ -23,6 +24,17 @@ pub const DB_FILE: &str = "inkwell.db";
|
||||
/// Attachment bytes, filed by content hash, beside [`DB_FILE`].
|
||||
pub const BLOBS_DIR: &str = "blobs";
|
||||
|
||||
/// An instant in the store's own form: RFC 3339, UTC, milliseconds, `Z`. Every
|
||||
/// stored time is written this way, which is what makes lexical order chronological.
|
||||
pub fn iso(t: DateTime<Utc>) -> String {
|
||||
t.to_rfc3339_opts(SecondsFormat::Millis, true)
|
||||
}
|
||||
|
||||
/// Now, in the store's own form ([`iso`]).
|
||||
pub fn now() -> String {
|
||||
iso(Utc::now())
|
||||
}
|
||||
|
||||
/// The shared database handle. rusqlite connections aren't `Sync`, so a `Mutex`
|
||||
/// serializes access — fine, since operations are quick and a client is single-user.
|
||||
/// How it is held is the caller's business: Tauri manages it as state, Android holds
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
use std::io::{Cursor, Read, Write};
|
||||
|
||||
use chrono::{DateTime, SecondsFormat, Utc};
|
||||
use chrono::{DateTime, Utc};
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Map, Value};
|
||||
@@ -21,7 +21,7 @@ use zip::write::SimpleFileOptions;
|
||||
use zip::{CompressionMethod, ZipArchive, ZipWriter};
|
||||
|
||||
use super::models::{Note, NoteCreateInput};
|
||||
use super::{derive, recur, store};
|
||||
use super::{derive, iso, now, recur, store};
|
||||
use crate::sync::blobs::BlobStore;
|
||||
|
||||
/// The `app` an export's notes.json carries. "thoughtsync" is what every export
|
||||
@@ -128,7 +128,7 @@ pub fn export_zip(conn: &Connection, blobs: &BlobStore) -> Result<Vec<u8>, Strin
|
||||
let doc = json!({
|
||||
"app": "inkwell",
|
||||
"version": 1,
|
||||
"exported_at": Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true),
|
||||
"exported_at": now(),
|
||||
"labels": labels,
|
||||
"notes": notes,
|
||||
});
|
||||
@@ -508,9 +508,7 @@ fn create_imported(
|
||||
|
||||
// Last, because adding an attachment touches the note: the source's own times are
|
||||
// what this note should carry, not the moment it was imported.
|
||||
let trashed_at = spec
|
||||
.trashed
|
||||
.then(|| Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true));
|
||||
let trashed_at = spec.trashed.then(now);
|
||||
conn.execute(
|
||||
"UPDATE notes SET pinned = ?1, archived = ?2, remind_at = ?3, recurrence = ?4,
|
||||
trashed = ?5, trashed_at = ?6,
|
||||
@@ -561,16 +559,13 @@ fn strings(v: Option<&Value>) -> Vec<String> {
|
||||
/// Any RFC 3339 instant, in the store's own form (UTC, milliseconds, `Z`). Anything
|
||||
/// else is treated as absent, as the server's `parse_dt` does.
|
||||
fn instant(raw: &str) -> Option<String> {
|
||||
DateTime::parse_from_rfc3339(raw).ok().map(|t| {
|
||||
t.with_timezone(&Utc)
|
||||
.to_rfc3339_opts(SecondsFormat::Millis, true)
|
||||
})
|
||||
DateTime::parse_from_rfc3339(raw).ok().map(|t| iso(t.with_timezone(&Utc)))
|
||||
}
|
||||
|
||||
/// Keep's timestamps: integer microseconds since the epoch.
|
||||
fn usec_instant(v: &Value) -> Option<String> {
|
||||
let usec = v.as_i64().or_else(|| v.as_str()?.parse().ok())?;
|
||||
DateTime::from_timestamp_micros(usec).map(|t| t.to_rfc3339_opts(SecondsFormat::Millis, true))
|
||||
DateTime::from_timestamp_micros(usec).map(iso)
|
||||
}
|
||||
|
||||
/// The image types the server infers from a name when the source gave no mime.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! ("YYYY-MM-DDTHH:MM:SS.sssZ") so string ordering and date-range comparisons line
|
||||
//! up with the values the frontend sends.
|
||||
|
||||
use chrono::{DateTime, Duration, SecondsFormat, Utc};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use rusqlite::{params, params_from_iter, Connection, OptionalExtension, Params};
|
||||
use serde_json::{json, Value};
|
||||
use uuid::Uuid;
|
||||
@@ -15,10 +15,7 @@ use uuid::Uuid;
|
||||
use crate::local::derive;
|
||||
use crate::local::models::*;
|
||||
use crate::local::recur;
|
||||
|
||||
fn now() -> String {
|
||||
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)
|
||||
}
|
||||
use crate::local::{iso, now};
|
||||
|
||||
fn new_id() -> String {
|
||||
Uuid::new_v4().to_string()
|
||||
@@ -571,8 +568,7 @@ fn should_snapshot(conn: &Connection, id: &str, new_body: &str) -> rusqlite::Res
|
||||
}
|
||||
// String comparison, not date maths: timestamps are RFC3339 UTC with a fixed
|
||||
// millisecond field (see the module header), so lexical order IS chronological.
|
||||
let cutoff = (Utc::now() - Duration::minutes(REVISION_WINDOW_MINUTES))
|
||||
.to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
let cutoff = iso(Utc::now() - Duration::minutes(REVISION_WINDOW_MINUTES));
|
||||
let recent: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM note_revisions WHERE note_id = ?1 AND created_at >= ?2",
|
||||
params![id, cutoff],
|
||||
@@ -700,7 +696,7 @@ pub fn complete_reminder(conn: &Connection, id: &str) -> rusqlite::Result<Note>
|
||||
match next {
|
||||
Some(at) => conn.execute(
|
||||
"UPDATE notes SET remind_at = ?1 WHERE id = ?2",
|
||||
params![at.to_rfc3339_opts(SecondsFormat::Millis, true), id],
|
||||
params![iso(at), id],
|
||||
)?,
|
||||
None => conn.execute(
|
||||
"UPDATE notes SET remind_at = NULL, recurrence = NULL WHERE id = ?1",
|
||||
@@ -719,7 +715,7 @@ pub fn snooze_reminder(conn: &Connection, id: &str, minutes: i64) -> rusqlite::R
|
||||
require_owner(conn, id)?;
|
||||
// 0 or less would set a reminder in the past; a huge value overflows `Duration`.
|
||||
let minutes = minutes.clamp(1, SNOOZE_MAX_MINUTES);
|
||||
let t = (Utc::now() + Duration::minutes(minutes)).to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
let t = iso(Utc::now() + Duration::minutes(minutes));
|
||||
conn.execute(
|
||||
"UPDATE notes SET remind_at = ?1 WHERE id = ?2",
|
||||
params![t, id],
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
//! own inside this crate, but exposing them separately would let a caller pull
|
||||
//! without pushing, which quietly overwrites unsent local edits.
|
||||
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use serde::Serialize;
|
||||
|
||||
use super::blobs::BlobStore;
|
||||
use super::pull;
|
||||
use super::push;
|
||||
use super::state;
|
||||
use crate::local::Db;
|
||||
use crate::local::{self, Db};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SyncOutcome {
|
||||
@@ -102,8 +101,7 @@ pub async fn run_cycle(
|
||||
}
|
||||
// Stamped only here, after BOTH halves succeeded. A timestamp written after a
|
||||
// partial cycle would tell the user they're up to date when they aren't.
|
||||
let now = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
state::mark_synced(&conn, &now).map_err(|e| e.to_string())?;
|
||||
state::mark_synced(&conn, &local::now()).map_err(|e| e.to_string())?;
|
||||
state::status(&conn).map_err(|e| e.to_string())?
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
//! transaction** as the page it describes — a cursor committed ahead of its data
|
||||
//! would silently skip those rows forever, which reads as a clean sync.
|
||||
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -14,7 +13,7 @@ use super::blobs::BlobStore;
|
||||
use super::client;
|
||||
use super::state;
|
||||
use super::wire;
|
||||
use crate::local::Db;
|
||||
use crate::local::{now, Db};
|
||||
|
||||
/// Backstop against a server that never stops saying `has_more`. At the server's
|
||||
/// 1000-row page cap this is 10M rows — far past any real store, so hitting it means
|
||||
@@ -108,10 +107,6 @@ async fn download_missing_blobs(
|
||||
Ok((downloaded, failed))
|
||||
}
|
||||
|
||||
fn now() -> String {
|
||||
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)
|
||||
}
|
||||
|
||||
/// Apply one page and advance the cursor, atomically.
|
||||
///
|
||||
/// Labels are applied before notes so a membership never references a label row that
|
||||
|
||||
Reference in New Issue
Block a user