refactor: rename size to uncompressed_size, add compressed_size and closed columns
Schema changes: - Rename items.size to items.uncompressed_size for clarity - Add compressed_size (INTEGER NULL) - tracks compressed file size on disk - Add closed (BOOLEAN NOT NULL DEFAULT 1) - tracks whether item is fully written - Existing items default to closed=true via migration Lifecycle: - Items created with closed=false, set to true on successful save/import - Compressed size captured via fs::metadata() after compression writer closes - Truncated uploads (413) get compressed_size set, closed=true, uncompressed_size=None - Update command now backfills both uncompressed_size and compressed_size Also includes bug fixes and dedup from prior review: - Fix stream_raw_content_response using uncompressed_size for raw byte Content-Length - ApiResponse::ok()/empty() constructors, TryFrom<ItemWithMeta> for ItemInfo - tag_names() method on ItemWithMeta, meta_filter() on Settings - Fix .unwrap() panics in compression engine Read/Write impls - Fix TOCTOU race in stream_raw_content_response (now uses compressed_size) - Fix swallowed write errors in meta plugins (digest, magic_file, exec) - Fix term::stderr().unwrap() panic in item_service - Deduplicate ItemService::new() calls across 20 API handlers - ImportMeta supports #[serde(alias = "size")] for backward compat All 75 tests, 67 doc tests pass. Clippy clean.
This commit is contained in:
103
src/db.rs
103
src/db.rs
@@ -19,7 +19,7 @@ and query utilities for efficient data access.
|
||||
# Schema
|
||||
|
||||
The database uses three main tables:
|
||||
- `items`: Core item information (ID, timestamp, size, compression).
|
||||
- `items`: Core item information (ID, timestamp, uncompressed_size, compressed_size, closed, compression).
|
||||
- `tags`: Item-tag associations (many-to-many).
|
||||
- `metas`: Item-metadata associations (many-to-many).
|
||||
|
||||
@@ -42,7 +42,7 @@ let conn = db::open(PathBuf::from("keep.db"))?;
|
||||
```
|
||||
Insert an item:
|
||||
```ignore
|
||||
let item = db::Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
let item = db::Item { id: None, ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
let id = db::insert_item(&conn, item)?;
|
||||
```
|
||||
*/
|
||||
@@ -78,6 +78,9 @@ lazy_static! {
|
||||
M::up("CREATE INDEX idx_tags_name ON tags(name)"),
|
||||
M::up("CREATE INDEX idx_metas_name ON metas(name)"),
|
||||
M::up("UPDATE items SET compression = 'raw' WHERE compression = 'none'"),
|
||||
M::up("ALTER TABLE items RENAME COLUMN size TO uncompressed_size"),
|
||||
M::up("ALTER TABLE items ADD COLUMN compressed_size INTEGER NULL"),
|
||||
M::up("ALTER TABLE items ADD COLUMN closed BOOLEAN NOT NULL DEFAULT 1"),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -89,7 +92,9 @@ lazy_static! {
|
||||
///
|
||||
/// * `id` - Unique identifier, `None` for new items.
|
||||
/// * `ts` - Creation timestamp in UTC.
|
||||
/// * `size` - Content size in bytes, `None` if not set.
|
||||
/// * `uncompressed_size` - Uncompressed content size in bytes, `None` if not set.
|
||||
/// * `compressed_size` - Compressed file size on disk, `None` if not set.
|
||||
/// * `closed` - Whether the item has been fully written and closed.
|
||||
/// * `compression` - Compression algorithm used.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Item {
|
||||
@@ -97,8 +102,12 @@ pub struct Item {
|
||||
pub id: Option<i64>,
|
||||
/// Timestamp when the item was created.
|
||||
pub ts: DateTime<Utc>,
|
||||
/// Size of the item content in bytes, None if not set.
|
||||
pub size: Option<i64>,
|
||||
/// Uncompressed size of the item content in bytes, None if not set.
|
||||
pub uncompressed_size: Option<i64>,
|
||||
/// Compressed file size on disk in bytes, None if not set.
|
||||
pub compressed_size: Option<i64>,
|
||||
/// Whether the item has been fully written and closed.
|
||||
pub closed: bool,
|
||||
/// Compression algorithm used for the item content.
|
||||
pub compression: String,
|
||||
}
|
||||
@@ -224,7 +233,9 @@ pub fn open(path: PathBuf) -> Result<Connection, Error> {
|
||||
/// let item = Item {
|
||||
/// id: None,
|
||||
/// ts: Utc::now(),
|
||||
/// size: None,
|
||||
/// uncompressed_size: None,
|
||||
/// compressed_size: None,
|
||||
/// closed: false,
|
||||
/// compression: "lz4".to_string(),
|
||||
/// };
|
||||
/// let id = db::insert_item(&conn, item)?;
|
||||
@@ -235,8 +246,8 @@ pub fn open(path: PathBuf) -> Result<Connection, Error> {
|
||||
pub fn insert_item(conn: &Connection, item: Item) -> Result<i64> {
|
||||
debug!("DB: Inserting item: {item:?}");
|
||||
conn.execute(
|
||||
"INSERT INTO items (ts, size, compression) VALUES (?1, ?2, ?3)",
|
||||
params![item.ts, item.size, item.compression],
|
||||
"INSERT INTO items (ts, uncompressed_size, compressed_size, closed, compression) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![item.ts, item.uncompressed_size, item.compressed_size, item.closed, item.compression],
|
||||
)?;
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
@@ -283,7 +294,9 @@ pub fn create_item(
|
||||
let item = Item {
|
||||
id: None,
|
||||
ts: chrono::Utc::now(),
|
||||
size: None,
|
||||
uncompressed_size: None,
|
||||
compressed_size: None,
|
||||
closed: false,
|
||||
compression: compression_type.to_string(),
|
||||
};
|
||||
let item_id = insert_item(conn, item.clone())?;
|
||||
@@ -312,7 +325,9 @@ pub fn insert_item_with_ts(
|
||||
let item = Item {
|
||||
id: None,
|
||||
ts,
|
||||
size: None,
|
||||
uncompressed_size: None,
|
||||
compressed_size: None,
|
||||
closed: false,
|
||||
compression: compression.to_string(),
|
||||
};
|
||||
let item_id = insert_item(conn, item.clone())?;
|
||||
@@ -353,7 +368,7 @@ pub fn insert_item_with_ts(
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: None, ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// let item_id = db::insert_item(&conn, item)?;
|
||||
/// db::add_tag(&conn, item_id, "important")?;
|
||||
/// # Ok(())
|
||||
@@ -411,7 +426,7 @@ pub fn upsert_tag(conn: &Connection, item_id: i64, tag_name: &str) -> Result<()>
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: None, ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// let item_id = db::insert_item(&conn, item)?;
|
||||
/// db::add_meta(&conn, item_id, "mime_type", "text/plain")?;
|
||||
/// # Ok(())
|
||||
@@ -456,7 +471,7 @@ pub fn add_meta(conn: &Connection, item_id: i64, name: &str, value: &str) -> Res
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: Some(1), size: Some(1024), compression: "lz4".to_string(), ts: Utc::now() };
|
||||
/// let item = Item { id: Some(1), ts: Utc::now(), uncompressed_size: Some(1024), compressed_size: Some(512), closed: true, compression: "lz4".to_string() };
|
||||
/// db::update_item(&conn, item)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
@@ -464,8 +479,8 @@ pub fn add_meta(conn: &Connection, item_id: i64, name: &str, value: &str) -> Res
|
||||
pub fn update_item(conn: &Connection, item: Item) -> Result<()> {
|
||||
debug!("DB: Updating item: {item:?}");
|
||||
conn.execute(
|
||||
"UPDATE items SET size=?2, compression=?3 WHERE id=?1",
|
||||
params![item.id, item.size, item.compression,],
|
||||
"UPDATE items SET uncompressed_size=?2, compressed_size=?3, closed=?4, compression=?5 WHERE id=?1",
|
||||
params![item.id, item.uncompressed_size, item.compressed_size, item.closed, item.compression,],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -500,7 +515,7 @@ pub fn update_item(conn: &Connection, item: Item) -> Result<()> {
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: Some(1), ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// db::delete_item(&conn, item)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
@@ -584,7 +599,7 @@ pub fn query_delete_meta(conn: &Connection, meta: Meta) -> Result<()> {
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: None, ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// let item_id = db::insert_item(&conn, item)?;
|
||||
/// let meta = Meta { id: item_id, name: "mime_type".to_string(), value: "text/plain".to_string() };
|
||||
/// db::query_upsert_meta(&conn, meta)?;
|
||||
@@ -630,7 +645,7 @@ pub fn query_upsert_meta(conn: &Connection, meta: Meta) -> Result<()> {
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: None, ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// let item_id = db::insert_item(&conn, item)?;
|
||||
/// // Insert new metadata
|
||||
/// let meta = Meta { id: item_id, name: "source".to_string(), value: "cli".to_string() };
|
||||
@@ -681,7 +696,7 @@ pub fn store_meta(conn: &Connection, meta: Meta) -> Result<()> {
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: None, ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// let item_id = db::insert_item(&conn, item)?;
|
||||
/// let tag = Tag { id: item_id, name: "work".to_string() };
|
||||
/// db::insert_tag(&conn, tag)?;
|
||||
@@ -726,7 +741,7 @@ pub fn insert_tag(conn: &Connection, tag: Tag) -> Result<()> {
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: Some(1), ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// db::delete_item_tags(&conn, item)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
@@ -768,9 +783,9 @@ pub fn delete_item_tags(conn: &Connection, item: Item) -> Result<()> {
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: None, ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// let item_id = db::insert_item(&conn, item)?;
|
||||
/// let item = Item { id: Some(item_id), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: Some(item_id), ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// let tags = vec!["project_a".to_string(), "urgent".to_string()];
|
||||
/// db::set_item_tags(&conn, item, &tags)?;
|
||||
/// # Ok(())
|
||||
@@ -831,7 +846,7 @@ pub fn set_item_tags(conn: &Connection, item: Item, tags: &Vec<String>) -> Resul
|
||||
pub fn query_all_items(conn: &Connection) -> Result<Vec<Item>> {
|
||||
debug!("DB: Querying all items");
|
||||
let mut statement = conn
|
||||
.prepare("SELECT id, ts, size, compression FROM items ORDER BY id ASC")
|
||||
.prepare("SELECT id, ts, uncompressed_size, compressed_size, closed, compression FROM items ORDER BY id ASC")
|
||||
.context("Problem preparing SQL statement")?;
|
||||
let mut rows = statement.query(params![])?;
|
||||
let mut items = Vec::new();
|
||||
@@ -840,8 +855,10 @@ pub fn query_all_items(conn: &Connection) -> Result<Vec<Item>> {
|
||||
let item = Item {
|
||||
id: row.get(0)?,
|
||||
ts: row.get(1)?,
|
||||
size: row.get(2)?,
|
||||
compression: row.get(3)?,
|
||||
uncompressed_size: row.get(2)?,
|
||||
compressed_size: row.get(3)?,
|
||||
closed: row.get(4)?,
|
||||
compression: row.get(5)?,
|
||||
};
|
||||
items.push(item);
|
||||
}
|
||||
@@ -889,7 +906,9 @@ pub fn query_tagged_items<'a>(conn: &'a Connection, tags: &'a Vec<String>) -> Re
|
||||
"
|
||||
SELECT items.id,
|
||||
items.ts,
|
||||
items.size,
|
||||
items.uncompressed_size,
|
||||
items.compressed_size,
|
||||
items.closed,
|
||||
items.compression,
|
||||
count(tags_match.id) as tags_score
|
||||
FROM items,
|
||||
@@ -915,8 +934,10 @@ pub fn query_tagged_items<'a>(conn: &'a Connection, tags: &'a Vec<String>) -> Re
|
||||
let item = Item {
|
||||
id: row.get(0)?,
|
||||
ts: row.get(1)?,
|
||||
size: row.get(2)?,
|
||||
compression: row.get(3)?,
|
||||
uncompressed_size: row.get(2)?,
|
||||
compressed_size: row.get(3)?,
|
||||
closed: row.get(4)?,
|
||||
compression: row.get(5)?,
|
||||
};
|
||||
items.push(item);
|
||||
}
|
||||
@@ -1107,7 +1128,7 @@ pub fn get_item_matching(
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: None, ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// let item_id = db::insert_item(&conn, item)?;
|
||||
/// let item = db::get_item(&conn, item_id)?;
|
||||
/// assert!(item.is_some());
|
||||
@@ -1119,7 +1140,7 @@ pub fn get_item(conn: &Connection, item_id: i64) -> Result<Option<Item>> {
|
||||
let mut statement = conn
|
||||
.prepare_cached(
|
||||
"
|
||||
SELECT id, ts, size, compression
|
||||
SELECT id, ts, uncompressed_size, compressed_size, closed, compression
|
||||
FROM items
|
||||
WHERE items.id = ?1",
|
||||
)
|
||||
@@ -1131,8 +1152,10 @@ pub fn get_item(conn: &Connection, item_id: i64) -> Result<Option<Item>> {
|
||||
Some(row) => Ok(Some(Item {
|
||||
id: row.get(0)?,
|
||||
ts: row.get(1)?,
|
||||
size: row.get(2)?,
|
||||
compression: row.get(3)?,
|
||||
uncompressed_size: row.get(2)?,
|
||||
compressed_size: row.get(3)?,
|
||||
closed: row.get(4)?,
|
||||
compression: row.get(5)?,
|
||||
})),
|
||||
None => Ok(None),
|
||||
}
|
||||
@@ -1174,7 +1197,7 @@ pub fn get_item_last(conn: &Connection) -> Result<Option<Item>> {
|
||||
let mut statement = conn
|
||||
.prepare_cached(
|
||||
"
|
||||
SELECT id, ts, size, compression
|
||||
SELECT id, ts, uncompressed_size, compressed_size, closed, compression
|
||||
FROM items
|
||||
ORDER BY id DESC
|
||||
LIMIT 1",
|
||||
@@ -1187,8 +1210,10 @@ pub fn get_item_last(conn: &Connection) -> Result<Option<Item>> {
|
||||
Some(row) => Ok(Some(Item {
|
||||
id: row.get(0)?,
|
||||
ts: row.get(1)?,
|
||||
size: row.get(2)?,
|
||||
compression: row.get(3)?,
|
||||
uncompressed_size: row.get(2)?,
|
||||
compressed_size: row.get(3)?,
|
||||
closed: row.get(4)?,
|
||||
compression: row.get(5)?,
|
||||
})),
|
||||
None => Ok(None),
|
||||
}
|
||||
@@ -1223,7 +1248,7 @@ pub fn get_item_last(conn: &Connection) -> Result<Option<Item>> {
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: Some(1), ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// let tags = db::get_item_tags(&conn, &item)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
@@ -1276,7 +1301,7 @@ pub fn get_item_tags(conn: &Connection, item: &Item) -> Result<Vec<Tag>> {
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: Some(1), ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// let meta = db::get_item_meta(&conn, &item)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
@@ -1331,7 +1356,7 @@ pub fn get_item_meta(conn: &Connection, item: &Item) -> Result<Vec<Meta>> {
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: Some(1), ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// let meta = db::get_item_meta_name(&conn, &item, "mime_type".to_string())?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
@@ -1383,7 +1408,7 @@ pub fn get_item_meta_name(conn: &Connection, item: &Item, name: String) -> Resul
|
||||
/// let _tmp = tempfile::tempdir()?;
|
||||
/// let db_path = _tmp.path().join("keep.db");
|
||||
/// let conn = db::open(db_path)?;
|
||||
/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
||||
/// let item = Item { id: Some(1), ts: Utc::now(), uncompressed_size: None, compressed_size: None, closed: false, compression: "lz4".to_string() };
|
||||
/// let value = db::get_item_meta_value(&conn, &item, "source".to_string())?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
|
||||
Reference in New Issue
Block a user