mirror of
https://github.com/PR0M3TH3AN/Marlin.git
synced 2025-09-08 07:08:44 +00:00
update
This commit is contained in:
28
README.md
28
README.md
@@ -30,7 +30,7 @@ _No cloud, no telemetry – your data never leaves the machine._
|
|||||||
▲ search / exec └──────┬──────┘
|
▲ search / exec └──────┬──────┘
|
||||||
└────────── backup / restore ▼
|
└────────── backup / restore ▼
|
||||||
timestamped snapshots
|
timestamped snapshots
|
||||||
````
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -56,34 +56,10 @@ cargo build --release
|
|||||||
sudo install -Dm755 target/release/marlin /usr/local/bin/marlin
|
sudo install -Dm755 target/release/marlin /usr/local/bin/marlin
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
```bash
|
For a concise walkthrough, see [Quick start & Demo](marlin_demo.md).
|
||||||
marlin init # create DB (idempotent)
|
|
||||||
marlin scan ~/Pictures ~/Documents # index files
|
|
||||||
marlin tag ~/Pictures/**/*.jpg photos/trip-2024 # add hierarchical tag
|
|
||||||
marlin attr set ~/Documents/**/*.pdf reviewed yes # set custom attribute
|
|
||||||
marlin search reviewed --exec "xdg-open {}" # open matches
|
|
||||||
marlin backup # snapshot DB
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Enable shell completions (optional but handy)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# create the directory if needed
|
|
||||||
mkdir -p ~/.config/bash_completion.d
|
|
||||||
|
|
||||||
# dump Bash completion
|
|
||||||
marlin completions bash > ~/.config/bash_completion.d/marlin
|
|
||||||
```
|
|
||||||
|
|
||||||
For Zsh, Fish, etc., redirect into your shell’s completions folder.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Database location
|
### Database location
|
||||||
|
|
||||||
|
@@ -1,9 +1,42 @@
|
|||||||
|
# Quick start & Demo
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# initialize the demo database
|
||||||
|
marlin init
|
||||||
|
|
||||||
|
# index only your demo folder
|
||||||
|
marlin scan ~/marlin_demo_complex
|
||||||
|
|
||||||
|
# tag all markdown in your demo Projects as “project/md”
|
||||||
|
marlin tag "~/marlin_demo_complex/Projects/**/*.md" project/md
|
||||||
|
|
||||||
|
# mark your demo reports as reviewed
|
||||||
|
marlin attr set "~/marlin_demo_complex/Reports/*.pdf" reviewed yes
|
||||||
|
|
||||||
|
# search for any reviewed files
|
||||||
|
marlin search "attr:reviewed=yes"
|
||||||
|
|
||||||
|
# snapshot the demo database
|
||||||
|
marlin backup
|
||||||
|
|
||||||
|
# test linking within your demo
|
||||||
|
touch ~/marlin_demo_complex/foo.txt ~/marlin_demo_complex/bar.txt
|
||||||
|
marlin scan ~/marlin_demo_complex
|
||||||
|
foo=~/marlin_demo_complex/foo.txt
|
||||||
|
bar=~/marlin_demo_complex/bar.txt
|
||||||
|
marlin link add "$foo" "$bar"
|
||||||
|
marlin link list "$foo"
|
||||||
|
marlin link backlinks "$bar"
|
||||||
|
````
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Marlin Demo
|
# Marlin Demo
|
||||||
|
|
||||||
Here’s a little “complex‐demo” you can spin up to exercise tags, attributes, FTS queries, `--exec` hooks, backups & restores. Just copy–paste each block into your terminal:
|
Here’s a little “complex‐demo” you can spin up to exercise tags, attributes, FTS queries, `--exec` hooks, backups & restores. Just copy–paste each block into your terminal:
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 0 Create the demo folder and some files
|
### 0 Create the demo folder and some files
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -148,18 +181,4 @@ marlin restore "$snap"
|
|||||||
|
|
||||||
# Confirm you still see “TODO”
|
# Confirm you still see “TODO”
|
||||||
marlin search TODO
|
marlin search TODO
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
That gives you:
|
|
||||||
|
|
||||||
* **wide folder structures** (Projects, Logs, Reports, Scripts, Media)
|
|
||||||
* **hierarchical tags** you can mix and match
|
|
||||||
* **key-value attributes** to flag state & review
|
|
||||||
* **FTS5 queries** with AND/OR/NOT
|
|
||||||
* **`--exec` hooks** to trigger external commands
|
|
||||||
* **JSON output** for programmatic gluing
|
|
||||||
* **backups & restores** to guard your data
|
|
||||||
|
|
||||||
Have fun playing around!
|
|
120
src/cli/link.rs
120
src/cli/link.rs
@@ -1,4 +1,6 @@
|
|||||||
// src/cli/link.rs
|
// src/cli/link.rs
|
||||||
|
|
||||||
|
use crate::db;
|
||||||
use clap::{Subcommand, Args};
|
use clap::{Subcommand, Args};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use crate::cli::Format;
|
use crate::cli::Format;
|
||||||
@@ -35,9 +37,119 @@ pub struct BacklinksArgs {
|
|||||||
|
|
||||||
pub fn run(cmd: &LinkCmd, conn: &mut Connection, format: Format) -> anyhow::Result<()> {
|
pub fn run(cmd: &LinkCmd, conn: &mut Connection, format: Format) -> anyhow::Result<()> {
|
||||||
match cmd {
|
match cmd {
|
||||||
LinkCmd::Add(args) => todo!("link add {:?}", args),
|
LinkCmd::Add(args) => {
|
||||||
LinkCmd::Rm(args) => todo!("link rm {:?}", args),
|
let src_id = db::file_id(conn, &args.from)?;
|
||||||
LinkCmd::List(args) => todo!("link list {:?}", args),
|
let dst_id = db::file_id(conn, &args.to)?;
|
||||||
LinkCmd::Backlinks(args) => todo!("link backlinks {:?}", args),
|
db::add_link(conn, src_id, dst_id, args.r#type.as_deref())?;
|
||||||
|
match format {
|
||||||
|
Format::Text => {
|
||||||
|
if let Some(t) = &args.r#type {
|
||||||
|
println!("Linked '{}' → '{}' [type='{}']", args.from, args.to, t);
|
||||||
|
} else {
|
||||||
|
println!("Linked '{}' → '{}'", args.from, args.to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Format::Json => {
|
||||||
|
let typ = args
|
||||||
|
.r#type
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| format!("\"{}\"", s))
|
||||||
|
.unwrap_or_else(|| "null".into());
|
||||||
|
println!(
|
||||||
|
"{{\"from\":\"{}\",\"to\":\"{}\",\"type\":{}}}",
|
||||||
|
args.from, args.to, typ
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinkCmd::Rm(args) => {
|
||||||
|
let src_id = db::file_id(conn, &args.from)?;
|
||||||
|
let dst_id = db::file_id(conn, &args.to)?;
|
||||||
|
db::remove_link(conn, src_id, dst_id, args.r#type.as_deref())?;
|
||||||
|
match format {
|
||||||
|
Format::Text => {
|
||||||
|
if let Some(t) = &args.r#type {
|
||||||
|
println!("Removed link '{}' → '{}' [type='{}']", args.from, args.to, t);
|
||||||
|
} else {
|
||||||
|
println!("Removed link '{}' → '{}'", args.from, args.to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Format::Json => {
|
||||||
|
let typ = args
|
||||||
|
.r#type
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| format!("\"{}\"", s))
|
||||||
|
.unwrap_or_else(|| "null".into());
|
||||||
|
println!(
|
||||||
|
"{{\"from\":\"{}\",\"to\":\"{}\",\"type\":{}}}",
|
||||||
|
args.from, args.to, typ
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinkCmd::List(args) => {
|
||||||
|
let results = db::list_links(
|
||||||
|
conn,
|
||||||
|
&args.pattern,
|
||||||
|
args.direction.as_deref(),
|
||||||
|
args.r#type.as_deref(),
|
||||||
|
)?;
|
||||||
|
match format {
|
||||||
|
Format::Json => {
|
||||||
|
let items: Vec<String> = results
|
||||||
|
.into_iter()
|
||||||
|
.map(|(src, dst, t)| {
|
||||||
|
let typ = t
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| format!("\"{}\"", s))
|
||||||
|
.unwrap_or_else(|| "null".into());
|
||||||
|
format!(
|
||||||
|
"{{\"from\":\"{}\",\"to\":\"{}\",\"type\":{}}}",
|
||||||
|
src, dst, typ
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
println!("[{}]", items.join(","));
|
||||||
|
}
|
||||||
|
Format::Text => {
|
||||||
|
for (src, dst, t) in results {
|
||||||
|
if let Some(t) = t {
|
||||||
|
println!("{} → {} [type='{}']", src, dst, t);
|
||||||
|
} else {
|
||||||
|
println!("{} → {}", src, dst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinkCmd::Backlinks(args) => {
|
||||||
|
let results = db::find_backlinks(conn, &args.pattern)?;
|
||||||
|
match format {
|
||||||
|
Format::Json => {
|
||||||
|
let items: Vec<String> = results
|
||||||
|
.into_iter()
|
||||||
|
.map(|(src, t)| {
|
||||||
|
let typ = t
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| format!("\"{}\"", s))
|
||||||
|
.unwrap_or_else(|| "null".into());
|
||||||
|
format!("{{\"from\":\"{}\",\"type\":{}}}", src, typ)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
println!("[{}]", items.join(","));
|
||||||
|
}
|
||||||
|
Format::Text => {
|
||||||
|
for (src, t) in results {
|
||||||
|
if let Some(t) = t {
|
||||||
|
println!("{} [type='{}']", src, t);
|
||||||
|
} else {
|
||||||
|
println!("{}", src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
28
src/db/migrations/0003_create_links_collections_views.sql
Normal file
28
src/db/migrations/0003_create_links_collections_views.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
|
||||||
|
-- File-to-file links
|
||||||
|
CREATE TABLE IF NOT EXISTS links (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
src_file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
||||||
|
dst_file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
||||||
|
type TEXT,
|
||||||
|
UNIQUE(src_file_id, dst_file_id, type)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Named collections
|
||||||
|
CREATE TABLE IF NOT EXISTS collections (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS collection_files (
|
||||||
|
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
||||||
|
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY(collection_id, file_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Saved views
|
||||||
|
CREATE TABLE IF NOT EXISTS views (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
query TEXT NOT NULL
|
||||||
|
);
|
106
src/db/mod.rs
106
src/db/mod.rs
@@ -1,4 +1,3 @@
|
|||||||
// src/db/mod.rs
|
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@@ -19,6 +18,7 @@ use tracing::{debug, info};
|
|||||||
const MIGRATIONS: &[(&str, &str)] = &[
|
const MIGRATIONS: &[(&str, &str)] = &[
|
||||||
("0001_initial_schema.sql", include_str!("migrations/0001_initial_schema.sql")),
|
("0001_initial_schema.sql", include_str!("migrations/0001_initial_schema.sql")),
|
||||||
("0002_update_fts_and_triggers.sql", include_str!("migrations/0002_update_fts_and_triggers.sql")),
|
("0002_update_fts_and_triggers.sql", include_str!("migrations/0002_update_fts_and_triggers.sql")),
|
||||||
|
("0003_create_links_collections_views.sql", include_str!("migrations/0003_create_links_collections_views.sql")),
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ─── connection bootstrap ──────────────────────────────────────────── */
|
/* ─── connection bootstrap ──────────────────────────────────────────── */
|
||||||
@@ -128,6 +128,99 @@ pub fn upsert_attr(conn: &Connection, file_id: i64, key: &str, value: &str) -> R
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a typed link from one file to another.
|
||||||
|
pub fn add_link(conn: &Connection, src_file_id: i64, dst_file_id: i64, link_type: Option<&str>) -> Result<()> {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO links(src_file_id, dst_file_id, type)
|
||||||
|
VALUES (?1, ?2, ?3)
|
||||||
|
ON CONFLICT(src_file_id, dst_file_id, type) DO NOTHING",
|
||||||
|
params![src_file_id, dst_file_id, link_type],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a typed link between two files.
|
||||||
|
pub fn remove_link(conn: &Connection, src_file_id: i64, dst_file_id: i64, link_type: Option<&str>) -> Result<()> {
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM links
|
||||||
|
WHERE src_file_id = ?1
|
||||||
|
AND dst_file_id = ?2
|
||||||
|
AND (type IS ?3 OR type = ?3)",
|
||||||
|
params![src_file_id, dst_file_id, link_type],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all links for files matching a glob-style pattern.
|
||||||
|
/// `direction` may be `"in"` (incoming), `"out"` (outgoing), or `None` (outgoing).
|
||||||
|
pub fn list_links(
|
||||||
|
conn: &Connection,
|
||||||
|
pattern: &str,
|
||||||
|
direction: Option<&str>,
|
||||||
|
link_type: Option<&str>,
|
||||||
|
) -> Result<Vec<(String, String, Option<String>)>> {
|
||||||
|
// Convert glob '*' → SQL LIKE '%'
|
||||||
|
let like_pattern = pattern.replace('*', "%");
|
||||||
|
|
||||||
|
// Find matching files
|
||||||
|
let mut stmt = conn.prepare("SELECT id, path FROM files WHERE path LIKE ?1")?;
|
||||||
|
let mut rows = stmt.query(params![like_pattern])?;
|
||||||
|
let mut files = Vec::new();
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
|
let id: i64 = row.get(0)?;
|
||||||
|
let path: String = row.get(1)?;
|
||||||
|
files.push((id, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results = Vec::new();
|
||||||
|
for (file_id, file_path) in files {
|
||||||
|
let (src_col, dst_col) = match direction {
|
||||||
|
Some("in") => ("dst_file_id", "src_file_id"),
|
||||||
|
_ => ("src_file_id", "dst_file_id"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sql = format!(
|
||||||
|
"SELECT f2.path, l.type
|
||||||
|
FROM links l
|
||||||
|
JOIN files f2 ON f2.id = l.{dst}
|
||||||
|
WHERE l.{src} = ?1
|
||||||
|
AND (?2 IS NULL OR l.type = ?2)",
|
||||||
|
src = src_col,
|
||||||
|
dst = dst_col,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut stmt2 = conn.prepare(&sql)?;
|
||||||
|
let mut rows2 = stmt2.query(params![file_id, link_type])?;
|
||||||
|
while let Some(r2) = rows2.next()? {
|
||||||
|
let other: String = r2.get(0)?;
|
||||||
|
let typ: Option<String> = r2.get(1)?;
|
||||||
|
results.push((file_path.clone(), other, typ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all incoming links (backlinks) to files matching a pattern.
|
||||||
|
pub fn find_backlinks(conn: &Connection, pattern: &str) -> Result<Vec<(String, Option<String>)>> {
|
||||||
|
let like_pattern = pattern.replace('*', "%");
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT f1.path, l.type
|
||||||
|
FROM links l
|
||||||
|
JOIN files f1 ON f1.id = l.src_file_id
|
||||||
|
JOIN files f2 ON f2.id = l.dst_file_id
|
||||||
|
WHERE f2.path LIKE ?1",
|
||||||
|
)?;
|
||||||
|
let mut rows = stmt.query(params![like_pattern])?;
|
||||||
|
let mut result = Vec::new();
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
|
let src_path: String = row.get(0)?;
|
||||||
|
let typ: Option<String> = row.get(1)?;
|
||||||
|
result.push((src_path, typ));
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── backup / restore ──────────────────────────────────────────────── */
|
/* ─── backup / restore ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
pub fn backup<P: AsRef<Path>>(db_path: P) -> Result<PathBuf> {
|
pub fn backup<P: AsRef<Path>>(db_path: P) -> Result<PathBuf> {
|
||||||
@@ -153,3 +246,14 @@ pub fn restore<P: AsRef<Path>>(backup_path: P, live_db_path: P) -> Result<()> {
|
|||||||
fs::copy(&backup_path, &live_db_path)?;
|
fs::copy(&backup_path, &live_db_path)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn migrations_apply_in_memory() {
|
||||||
|
// Opening an in-memory database should apply every migration without error.
|
||||||
|
let _conn = open(":memory:").expect("in-memory migrations should run cleanly");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
28
src/main.rs
28
src/main.rs
@@ -29,7 +29,6 @@ fn main() -> Result<()> {
|
|||||||
// If the user asked for completions, generate and exit immediately.
|
// If the user asked for completions, generate and exit immediately.
|
||||||
if let Commands::Completions { shell } = &args.command {
|
if let Commands::Completions { shell } = &args.command {
|
||||||
let mut cmd = Cli::command();
|
let mut cmd = Cli::command();
|
||||||
// Shell is Copy so we can deref it safely
|
|
||||||
generate(*shell, &mut cmd, "marlin", &mut io::stdout());
|
generate(*shell, &mut cmd, "marlin", &mut io::stdout());
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -50,9 +49,7 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
// Dispatch all commands
|
// Dispatch all commands
|
||||||
match args.command {
|
match args.command {
|
||||||
Commands::Completions { .. } => {
|
Commands::Completions { .. } => {}
|
||||||
// no-op, already handled above
|
|
||||||
}
|
|
||||||
Commands::Init => {
|
Commands::Init => {
|
||||||
info!("Database initialised at {}", cfg.db_path.display());
|
info!("Database initialised at {}", cfg.db_path.display());
|
||||||
}
|
}
|
||||||
@@ -93,7 +90,6 @@ fn main() -> Result<()> {
|
|||||||
.with_context(|| format!("Could not open restored DB at {}", cfg.db_path.display()))?;
|
.with_context(|| format!("Could not open restored DB at {}", cfg.db_path.display()))?;
|
||||||
info!("Successfully opened restored database.");
|
info!("Successfully opened restored database.");
|
||||||
}
|
}
|
||||||
// new domains delegate to their run() functions
|
|
||||||
Commands::Link(link_cmd) => cli::link::run(&link_cmd, &mut conn, args.format)?,
|
Commands::Link(link_cmd) => cli::link::run(&link_cmd, &mut conn, args.format)?,
|
||||||
Commands::Coll(coll_cmd) => cli::coll::run(&coll_cmd, &mut conn, args.format)?,
|
Commands::Coll(coll_cmd) => cli::coll::run(&coll_cmd, &mut conn, args.format)?,
|
||||||
Commands::View(view_cmd) => cli::view::run(&view_cmd, &mut conn, args.format)?,
|
Commands::View(view_cmd) => cli::view::run(&view_cmd, &mut conn, args.format)?,
|
||||||
@@ -221,6 +217,8 @@ fn attr_ls(conn: &rusqlite::Connection, path: &std::path::Path) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build and run an FTS5 search query, with optional exec.
|
/// Build and run an FTS5 search query, with optional exec.
|
||||||
|
/// Now splits “tag:foo/bar” into `tags_text:foo AND tags_text:bar`
|
||||||
|
/// and “attr:key=value” into `attrs_text:key AND attrs_text:value`.
|
||||||
fn run_search(conn: &rusqlite::Connection, raw_query: &str, exec: Option<String>) -> Result<()> {
|
fn run_search(conn: &rusqlite::Connection, raw_query: &str, exec: Option<String>) -> Result<()> {
|
||||||
let mut fts_query_parts = Vec::new();
|
let mut fts_query_parts = Vec::new();
|
||||||
let parts = shlex::split(raw_query).unwrap_or_else(|| vec![raw_query.to_string()]);
|
let parts = shlex::split(raw_query).unwrap_or_else(|| vec![raw_query.to_string()]);
|
||||||
@@ -228,9 +226,21 @@ fn run_search(conn: &rusqlite::Connection, raw_query: &str, exec: Option<String>
|
|||||||
if ["AND", "OR", "NOT"].contains(&part.as_str()) {
|
if ["AND", "OR", "NOT"].contains(&part.as_str()) {
|
||||||
fts_query_parts.push(part);
|
fts_query_parts.push(part);
|
||||||
} else if let Some(tag) = part.strip_prefix("tag:") {
|
} else if let Some(tag) = part.strip_prefix("tag:") {
|
||||||
fts_query_parts.push(format!("tags_text:{}", escape_fts_query_term(tag)));
|
let segments: Vec<&str> = tag.split('/').filter(|s| !s.is_empty()).collect();
|
||||||
|
for (i, seg) in segments.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
fts_query_parts.push("AND".into());
|
||||||
|
}
|
||||||
|
fts_query_parts.push(format!("tags_text:{}", escape_fts_query_term(seg)));
|
||||||
|
}
|
||||||
} else if let Some(attr) = part.strip_prefix("attr:") {
|
} else if let Some(attr) = part.strip_prefix("attr:") {
|
||||||
fts_query_parts.push(format!("attrs_text:{}", escape_fts_query_term(attr)));
|
if let Some((k, v)) = attr.split_once('=') {
|
||||||
|
fts_query_parts.push(format!("attrs_text:{}", escape_fts_query_term(k)));
|
||||||
|
fts_query_parts.push("AND".into());
|
||||||
|
fts_query_parts.push(format!("attrs_text:{}", escape_fts_query_term(v)));
|
||||||
|
} else {
|
||||||
|
fts_query_parts.push(format!("attrs_text:{}", escape_fts_query_term(attr)));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fts_query_parts.push(escape_fts_query_term(&part));
|
fts_query_parts.push(escape_fts_query_term(&part));
|
||||||
}
|
}
|
||||||
@@ -302,7 +312,7 @@ fn run_search(conn: &rusqlite::Connection, raw_query: &str, exec: Option<String>
|
|||||||
/// Quote terms for FTS when needed.
|
/// Quote terms for FTS when needed.
|
||||||
fn escape_fts_query_term(term: &str) -> String {
|
fn escape_fts_query_term(term: &str) -> String {
|
||||||
if term.contains(|c: char| c.is_whitespace() || "-:()\"".contains(c))
|
if term.contains(|c: char| c.is_whitespace() || "-:()\"".contains(c))
|
||||||
|| ["AND","OR","NOT","NEAR"].contains(&term.to_uppercase().as_str())
|
|| ["AND", "OR", "NOT", "NEAR"].contains(&term.to_uppercase().as_str())
|
||||||
{
|
{
|
||||||
format!("\"{}\"", term.replace('"', "\"\""))
|
format!("\"{}\"", term.replace('"', "\"\""))
|
||||||
} else {
|
} else {
|
||||||
@@ -315,7 +325,7 @@ fn determine_scan_root(pattern: &str) -> PathBuf {
|
|||||||
let wildcard_pos = pattern.find(|c| c == '*' || c == '?' || c == '[').unwrap_or(pattern.len());
|
let wildcard_pos = pattern.find(|c| c == '*' || c == '?' || c == '[').unwrap_or(pattern.len());
|
||||||
let prefix = &pattern[..wildcard_pos];
|
let prefix = &pattern[..wildcard_pos];
|
||||||
let mut root = PathBuf::from(prefix);
|
let mut root = PathBuf::from(prefix);
|
||||||
while root.as_os_str().to_string_lossy().contains(|c| ['*','?','['].contains(&c)) {
|
while root.as_os_str().to_string_lossy().contains(|c| ['*', '?', '['].contains(&c)) {
|
||||||
if let Some(parent) = root.parent() {
|
if let Some(parent) = root.parent() {
|
||||||
root = parent.to_path_buf();
|
root = parent.to_path_buf();
|
||||||
} else {
|
} else {
|
||||||
|
Binary file not shown.
@@ -1 +1 @@
|
|||||||
/home/user/Documents/GitHub/Marlin/target/release/marlin: /home/user/Documents/GitHub/Marlin/src/cli/annotate.rs /home/user/Documents/GitHub/Marlin/src/cli/coll.rs /home/user/Documents/GitHub/Marlin/src/cli/event.rs /home/user/Documents/GitHub/Marlin/src/cli/link.rs /home/user/Documents/GitHub/Marlin/src/cli/remind.rs /home/user/Documents/GitHub/Marlin/src/cli/state.rs /home/user/Documents/GitHub/Marlin/src/cli/task.rs /home/user/Documents/GitHub/Marlin/src/cli/version.rs /home/user/Documents/GitHub/Marlin/src/cli/view.rs /home/user/Documents/GitHub/Marlin/src/cli.rs /home/user/Documents/GitHub/Marlin/src/config.rs /home/user/Documents/GitHub/Marlin/src/db/migrations/0001_initial_schema.sql /home/user/Documents/GitHub/Marlin/src/db/migrations/0002_update_fts_and_triggers.sql /home/user/Documents/GitHub/Marlin/src/db/mod.rs /home/user/Documents/GitHub/Marlin/src/logging.rs /home/user/Documents/GitHub/Marlin/src/main.rs /home/user/Documents/GitHub/Marlin/src/scan.rs
|
/home/user/Documents/GitHub/Marlin/target/release/marlin: /home/user/Documents/GitHub/Marlin/src/cli/annotate.rs /home/user/Documents/GitHub/Marlin/src/cli/coll.rs /home/user/Documents/GitHub/Marlin/src/cli/event.rs /home/user/Documents/GitHub/Marlin/src/cli/link.rs /home/user/Documents/GitHub/Marlin/src/cli/remind.rs /home/user/Documents/GitHub/Marlin/src/cli/state.rs /home/user/Documents/GitHub/Marlin/src/cli/task.rs /home/user/Documents/GitHub/Marlin/src/cli/version.rs /home/user/Documents/GitHub/Marlin/src/cli/view.rs /home/user/Documents/GitHub/Marlin/src/cli.rs /home/user/Documents/GitHub/Marlin/src/config.rs /home/user/Documents/GitHub/Marlin/src/db/migrations/0001_initial_schema.sql /home/user/Documents/GitHub/Marlin/src/db/migrations/0002_update_fts_and_triggers.sql /home/user/Documents/GitHub/Marlin/src/db/migrations/0003_create_links_collections_views.sql /home/user/Documents/GitHub/Marlin/src/db/mod.rs /home/user/Documents/GitHub/Marlin/src/logging.rs /home/user/Documents/GitHub/Marlin/src/main.rs /home/user/Documents/GitHub/Marlin/src/scan.rs
|
||||||
|
Reference in New Issue
Block a user