mirror of
https://github.com/NotAShelf/stash.git
synced 2026-06-13 16:35:05 +00:00
Compare commits
No commits in common. "main" and "notashelf/push-vsvputymsntr" have entirely different histories.
main
...
notashelf/
8 changed files with 45 additions and 199 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -2937,7 +2937,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stash-clipboard"
|
name = "stash-clipboard"
|
||||||
version = "0.4.0"
|
version = "0.3.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"age",
|
"age",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "stash-clipboard"
|
name = "stash-clipboard"
|
||||||
description = "Wayland clipboard manager with fast persistent history and multi-media support"
|
description = "Wayland clipboard manager with fast persistent history and multi-media support"
|
||||||
version = "0.4.0"
|
version = "0.3.6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = [ "NotAShelf <raf@notashelf.dev>" ]
|
authors = [ "NotAShelf <raf@notashelf.dev>" ]
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
|
|
|
||||||
120
README.md
120
README.md
|
|
@ -20,10 +20,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
Lightweight & feature-rich Wayland clipboard "manager" with fast persistent
|
Lightweight & feature-rich Wayland clipboard "manager" with fast persistent history and
|
||||||
history and robust multi-media support. Stores and previews clipboard
|
robust multi-media support. Stores and previews clipboard entries (text, images)
|
||||||
entries (text, images) on the clipboard with a neat TUI and advanced
|
on the clipboard with a neat TUI and advanced scripting capabilities.
|
||||||
scripting capabilities.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
@ -53,8 +52,6 @@ with many features such as but not necessarily limited to:
|
||||||
- Drop-in replacement for `wl-clipboard` tools (`wl-copy` and `wl-paste`)
|
- Drop-in replacement for `wl-clipboard` tools (`wl-copy` and `wl-paste`)
|
||||||
- Sensitive clipboard filtering via regex (see below)
|
- Sensitive clipboard filtering via regex (see below)
|
||||||
- Sensitive clipboard filtering by application (see below)
|
- Sensitive clipboard filtering by application (see below)
|
||||||
- Password manager hint filtering (`x-kde-passwordManagerHint`)
|
|
||||||
- Optional at-rest encryption for database entries using age
|
|
||||||
|
|
||||||
on top of the existing features of Cliphist, which are as follows:
|
on top of the existing features of Cliphist, which are as follows:
|
||||||
|
|
||||||
|
|
@ -273,8 +270,8 @@ stash db stats
|
||||||
- `stash db vacuum`: Optimize the database using SQLite's VACUUM command,
|
- `stash db vacuum`: Optimize the database using SQLite's VACUUM command,
|
||||||
reclaiming space and improving performance.
|
reclaiming space and improving performance.
|
||||||
- `stash db stats`: Display database statistics including total/active/expired
|
- `stash db stats`: Display database statistics including total/active/expired
|
||||||
entry counts, encrypted/undecryptable entry counts, storage size, and page
|
entry counts, storage size, and page information. This is provided purely for
|
||||||
information.
|
convenience and the rule of the cool.
|
||||||
|
|
||||||
### Watch clipboard for changes and store automatically
|
### Watch clipboard for changes and store automatically
|
||||||
|
|
||||||
|
|
@ -360,36 +357,21 @@ sensitive pattern, using a regular expression. This is useful for preventing
|
||||||
accidental storage of secrets, passwords, or other sensitive data. You don't
|
accidental storage of secrets, passwords, or other sensitive data. You don't
|
||||||
want sensitive data ending up in your persistent clipboard, right?
|
want sensitive data ending up in your persistent clipboard, right?
|
||||||
|
|
||||||
The filter can be configured in several ways, as part of three separate
|
The filter can be configured in one of three ways, as part of two separate
|
||||||
features.
|
features.
|
||||||
|
|
||||||
#### Clipboard Filtering by Entry Regex
|
#### Clipboard Filtering by Entry Regex
|
||||||
|
|
||||||
This can be configured in several ways. The simplest is the **environment
|
This can be configured in one of two ways. You can use the **environment
|
||||||
variable** `STASH_SENSITIVE_REGEX` set to a valid regex pattern; if the
|
variable** `STASTH_SENSITIVE_REGEX` to a valid regex pattern, and if the
|
||||||
clipboard text matches, it will not be stored. Useful for trivial secrets such
|
clipboard text matches the regex it will not be stored. This can be used for
|
||||||
as GitHub tokens or secrets that follow a rule. You would typically set this in
|
trivial secrets such as but not limited to GitHub tokens or secrets that follow
|
||||||
your `~/.bashrc` or similar, but in some cases this might be a security flaw.
|
a rule, e.g. a prefix. You would typically set this in your `~/.bashrc` or
|
||||||
|
similar but in some cases this might be a security flaw.
|
||||||
|
|
||||||
The less-insecure alternatives are:
|
The safer alternative to this is using **Systemd LoadCrediental**. If Stash is
|
||||||
|
running as a Systemd service, you can provide a regex pattern using a crediental
|
||||||
- `STASH_SENSITIVE_REGEX_FILE`: read the regex from a file path. Useful with
|
file. For example, add to your `stash.service`:
|
||||||
NixOS secrets managers like agenix or sops-nix.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export STASH_SENSITIVE_REGEX_FILE=/run/secrets/stash/clipboard_filter
|
|
||||||
```
|
|
||||||
|
|
||||||
- `STASH_SENSITIVE_REGEX_COMMAND`: execute a shell command whose stdout is the
|
|
||||||
regex pattern. Works well with password managers.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export STASH_SENSITIVE_REGEX_COMMAND="pass show stash/clipboard-filter"
|
|
||||||
```
|
|
||||||
|
|
||||||
The safest option is **Systemd LoadCredential**. If Stash is running as a
|
|
||||||
Systemd service, you can provide a regex pattern using a credential file. For
|
|
||||||
example, add to your `stash.service`:
|
|
||||||
|
|
||||||
```dosini
|
```dosini
|
||||||
LoadCredential=clipboard_filter:/etc/stash/clipboard_filter
|
LoadCredential=clipboard_filter:/etc/stash/clipboard_filter
|
||||||
|
|
@ -400,9 +382,9 @@ quotes). This is done automatically in the
|
||||||
[vendored Systemd service](./contrib/stash.service). Remember to set the
|
[vendored Systemd service](./contrib/stash.service). Remember to set the
|
||||||
appropriate file permissions if using this option.
|
appropriate file permissions if using this option.
|
||||||
|
|
||||||
The service will check the credential file first, then the command, then the
|
The service will check the credential file first, then the environment variable.
|
||||||
file path, then the environment variable. If a clipboard entry matches the
|
If a clipboard entry matches the regex, it will be skipped and a warning will be
|
||||||
regex, it will be skipped and a warning will be logged.
|
logged.
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> **Example regex to block common password patterns**:
|
> **Example regex to block common password patterns**:
|
||||||
|
|
@ -433,72 +415,6 @@ be only copied to the clipboard.
|
||||||
>
|
>
|
||||||
> `stash --excluded-apps Bitwarden watch`
|
> `stash --excluded-apps Bitwarden watch`
|
||||||
|
|
||||||
#### Clipboard Filtering by Password Manager Hint
|
|
||||||
|
|
||||||
Stash automatically skips entries whose clipboard offer includes the
|
|
||||||
`x-kde-passwordManagerHint` MIME type. This is the convention used by KeePassXC
|
|
||||||
and compatible password managers to signal that clipboard content is sensitive
|
|
||||||
and should not be persisted.
|
|
||||||
|
|
||||||
No configuration is required. If the hint is present in the clipboard offer, the
|
|
||||||
entry is dropped before storage. The entry is still available in your clipboard
|
|
||||||
— it is only excluded from the persistent database.
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> This filter only applies via the watch daemon (`stash watch`), where MIME type
|
|
||||||
> metadata is available from the Wayland clipboard protocol. Manual
|
|
||||||
> `stash store` invocations do not have this context and are not filtered.
|
|
||||||
|
|
||||||
### Database Encryption
|
|
||||||
|
|
||||||
Stash supports encrypting clipboard entries at rest using the
|
|
||||||
[age](https://age-encryption.org/) encryption format.
|
|
||||||
|
|
||||||
Encryption is **opt-in** and only activates when a passphrase is configured.
|
|
||||||
When one is configured, all new entries are encrypted before storage and
|
|
||||||
decrypted transparently on retrieval. Entries stored without encryption remain
|
|
||||||
as plaintext. Only new entries written after configuring encryption are
|
|
||||||
encrypted.
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> Removing the passphrase after encrypted entries have been stored leaves those
|
|
||||||
> entries permanently unreadable. There is no migration path short of wiping the
|
|
||||||
> database. `stash db stats` reports affected entries as Undecryptable.
|
|
||||||
>
|
|
||||||
> Full-text search (`stash delete --type query`, TUI search) operates on raw
|
|
||||||
> database contents. Encrypted entries will not match any search query.
|
|
||||||
|
|
||||||
#### Configuration
|
|
||||||
|
|
||||||
Provide a passphrase in one of these ways (checked in order):
|
|
||||||
|
|
||||||
1. **Systemd LoadCredential** (safest): add to `stash.service`:
|
|
||||||
|
|
||||||
```dosini
|
|
||||||
LoadCredential=stash_encryption_passphrase:/etc/stash/encryption_passphrase
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Command** — stdout of a shell command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export STASH_ENCRYPTION_PASSPHRASE_COMMAND="pass show stash/encryption-key"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **File** — path to a file containing the passphrase:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export STASH_ENCRYPTION_PASSPHRASE_FILE=/run/secrets/stash/encryption_passphrase
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Environment variable** (least secure):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export STASH_ENCRYPTION_PASSPHRASE="your-secure-passphrase"
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> Back up your passphrase. Encrypted entries cannot be recovered without it.
|
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
I've been a long-time user of Cliphist. You can probably tell by the number of
|
I've been a long-time user of Cliphist. You can probably tell by the number of
|
||||||
|
|
|
||||||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"crane": {
|
"crane": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1780532242,
|
"lastModified": 1778106249,
|
||||||
"narHash": "sha256-D+BsdpxmtUwtqGoY0IXPhHgTlmqgcZKCEo1oMyn7ep0=",
|
"narHash": "sha256-cM/AuKy5tMhwOOQIbha8ZRRMHVfNf7cv2aljIw+qoCg=",
|
||||||
"owner": "ipetkov",
|
"owner": "ipetkov",
|
||||||
"repo": "crane",
|
"repo": "crane",
|
||||||
"rev": "59a82a1222dd3b2080b5cc52a1a2e8d5f1b77f37",
|
"rev": "6d015ea29630b7ad2402841386da2cb617a470a7",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ self: {
|
||||||
...
|
...
|
||||||
}: let
|
}: let
|
||||||
inherit (lib.modules) mkIf;
|
inherit (lib.modules) mkIf;
|
||||||
inherit (lib.options) mkOption mkEnableOption mkPackageOption;
|
inherit (lib.options) mkOption mkEnableOption mkPackageOption literalMD;
|
||||||
inherit (lib.types) listOf str;
|
inherit (lib.types) listOf str;
|
||||||
inherit (lib.strings) concatStringsSep;
|
inherit (lib.strings) concatStringsSep;
|
||||||
inherit (lib.meta) getExe;
|
inherit (lib.meta) getExe;
|
||||||
|
|
@ -15,9 +15,7 @@ in {
|
||||||
options.services.stash-clipboard = {
|
options.services.stash-clipboard = {
|
||||||
enable = mkEnableOption "stash, a Wayland clipboard manager";
|
enable = mkEnableOption "stash, a Wayland clipboard manager";
|
||||||
|
|
||||||
package = mkPackageOption self.packages.${pkgs.stdenv.hostPlatform.system} ["stash"] {
|
package = mkPackageOption self.packages.${pkgs.system} ["stash"] {};
|
||||||
pkgsText = "self.packages.\${pkgs.stdenv.hostPlatform.system}";
|
|
||||||
};
|
|
||||||
|
|
||||||
flags = mkOption {
|
flags = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
|
|
@ -30,7 +28,7 @@ in {
|
||||||
type = str;
|
type = str;
|
||||||
default = "";
|
default = "";
|
||||||
example = "{file}`/etc/stash/clipboard_filter`";
|
example = "{file}`/etc/stash/clipboard_filter`";
|
||||||
description = ''
|
description = literalMD ''
|
||||||
File containing a regular expression to catch sensitive patterns. The file
|
File containing a regular expression to catch sensitive patterns. The file
|
||||||
passed to this option must contain your regex pattern with no quotes.
|
passed to this option must contain your regex pattern with no quotes.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,25 +22,9 @@ static SERVING_PID: AtomicI32 = AtomicI32::new(0);
|
||||||
|
|
||||||
/// Get the current serving PID if any. Used by the watch loop to avoid
|
/// Get the current serving PID if any. Used by the watch loop to avoid
|
||||||
/// duplicate persistence processes.
|
/// duplicate persistence processes.
|
||||||
///
|
|
||||||
/// Probes the stored PID with `kill(pid, 0)` to detect children that have
|
|
||||||
/// already exited (SIGCHLD is ignored so we never get reaped notifications).
|
|
||||||
/// A stale PID is cleared and `None` is returned.
|
|
||||||
pub fn get_serving_pid() -> Option<i32> {
|
pub fn get_serving_pid() -> Option<i32> {
|
||||||
let pid = SERVING_PID.load(Ordering::SeqCst);
|
let pid = SERVING_PID.load(Ordering::SeqCst);
|
||||||
if pid == 0 {
|
if pid != 0 { Some(pid) } else { None }
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signal 0 = existence check, no signal sent. Returns 0 if alive,
|
|
||||||
// -1 (ESRCH) if the PID is gone.
|
|
||||||
if unsafe { libc::kill(pid, 0) } == 0 {
|
|
||||||
Some(pid)
|
|
||||||
} else {
|
|
||||||
let _ =
|
|
||||||
SERVING_PID.compare_exchange(pid, 0, Ordering::SeqCst, Ordering::SeqCst);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result type for persistence operations.
|
/// Result type for persistence operations.
|
||||||
|
|
@ -173,18 +157,6 @@ unsafe fn fork_and_serve(prepared: PreparedCopy) -> PersistenceResult<()> {
|
||||||
libc::signal(libc::SIGCHLD, libc::SIG_IGN);
|
libc::signal(libc::SIGCHLD, libc::SIG_IGN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace any prior serving child: a new clipboard entry supersedes the
|
|
||||||
// old offer (the compositor will invalidate it anyway the moment the new
|
|
||||||
// selection is taken). Without this, the old child lingers serving stale
|
|
||||||
// data until MAX_SERVE_REQUESTS or invalidation.
|
|
||||||
let prior = SERVING_PID.swap(0, Ordering::SeqCst);
|
|
||||||
if prior > 0 && unsafe { libc::kill(prior, 0) } == 0 {
|
|
||||||
unsafe {
|
|
||||||
libc::kill(prior, libc::SIGTERM);
|
|
||||||
}
|
|
||||||
log::debug!("terminated prior persistence child (pid: {prior})");
|
|
||||||
}
|
|
||||||
|
|
||||||
match unsafe { libc::fork() } {
|
match unsafe { libc::fork() } {
|
||||||
0 => {
|
0 => {
|
||||||
// Child process - clear serving PID
|
// Child process - clear serving PID
|
||||||
|
|
|
||||||
|
|
@ -1024,17 +1024,6 @@ impl SqliteClipboardDb {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clean up all expired entries. Returns count deleted.
|
/// Clean up all expired entries. Returns count deleted.
|
||||||
pub fn expire_ttl_entries(&self) -> Result<usize, StashError> {
|
|
||||||
self
|
|
||||||
.conn
|
|
||||||
.execute(
|
|
||||||
"UPDATE clipboard SET is_expired = 1 WHERE expires_at IS NOT NULL AND \
|
|
||||||
(is_expired IS NULL OR is_expired = 0)",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.map_err(|e| StashError::Trim(e.to_string().into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cleanup_expired(&self) -> Result<usize, StashError> {
|
pub fn cleanup_expired(&self) -> Result<usize, StashError> {
|
||||||
let now = Self::now();
|
let now = Self::now();
|
||||||
self
|
self
|
||||||
|
|
@ -1152,15 +1141,13 @@ impl SqliteClipboardDb {
|
||||||
#[cfg(not(feature = "encryption"))]
|
#[cfg(not(feature = "encryption"))]
|
||||||
let undecryptable: i64 = encrypted;
|
let undecryptable: i64 = encrypted;
|
||||||
|
|
||||||
let db_path = self.db_path.display();
|
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
"Database Statistics:\n\nEntries:\nTotal: \
|
"Database Statistics:\n\nEntries:\nTotal: \
|
||||||
{total}\nActive: {active}\nExpired: \
|
{total}\nActive: {active}\nExpired: \
|
||||||
{expired}\nWith TTL: \
|
{expired}\nWith TTL: \
|
||||||
{with_expiration}\nEncrypted: \
|
{with_expiration}\nEncrypted: \
|
||||||
{encrypted}\nUndecryptable: \
|
{encrypted}\nUndecryptable: \
|
||||||
{undecryptable}\n\nStorage:\nPath: \
|
{undecryptable}\n\nStorage:\nSize: {size_mb:.2} MB \
|
||||||
{db_path}\nSize: {size_mb:.2} MB \
|
|
||||||
({size_bytes} bytes)\nPages: {page_count}\nPage size: \
|
({size_bytes} bytes)\nPages: {page_count}\nPage size: \
|
||||||
{page_size} bytes"
|
{page_size} bytes"
|
||||||
))
|
))
|
||||||
|
|
@ -1231,11 +1218,6 @@ fn load_sensitive_regex() -> Option<Regex> {
|
||||||
/// previously encrypted entries permanently undecryptable, so the permanent
|
/// previously encrypted entries permanently undecryptable, so the permanent
|
||||||
/// cache prevents accidental passphrase changes from corrupting the
|
/// cache prevents accidental passphrase changes from corrupting the
|
||||||
/// clipboard history.
|
/// clipboard history.
|
||||||
///
|
|
||||||
/// Removing the passphrase entirely (disabling encryption) after entries have
|
|
||||||
/// been stored encrypted also renders those entries permanently unreadable.
|
|
||||||
/// There is no migration path short of wiping the database. `stash stats`
|
|
||||||
/// reports affected entries as Undecryptable.
|
|
||||||
#[cfg(feature = "encryption")]
|
#[cfg(feature = "encryption")]
|
||||||
fn load_encryption_passphrase() -> Option<age::secrecy::SecretString> {
|
fn load_encryption_passphrase() -> Option<age::secrecy::SecretString> {
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
@ -1268,18 +1250,25 @@ fn load_encryption_passphrase() -> Option<age::secrecy::SecretString> {
|
||||||
Some(secret)
|
Some(secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypt age-encrypted data.
|
/// Decrypt age-encrypted data using a cached scrypt identity.
|
||||||
///
|
|
||||||
/// `age::scrypt::Identity::new` is cheap since it stores the passphrase only.
|
|
||||||
/// The scrypt KDF runs inside `age::decrypt` per call, on the per-file salt
|
|
||||||
/// embedded in the ciphertext header. Caching the Identity would not avoid
|
|
||||||
/// it. The passphrase itself is cached by [`load_encryption_passphrase`].
|
|
||||||
#[cfg(feature = "encryption")]
|
#[cfg(feature = "encryption")]
|
||||||
fn decrypt_cached(ciphertext: &[u8]) -> Result<Vec<u8>, StashError> {
|
fn decrypt_cached(ciphertext: &[u8]) -> Result<Vec<u8>, StashError> {
|
||||||
let passphrase = load_encryption_passphrase()
|
static CACHE: OnceLock<Mutex<Option<age::scrypt::Identity>>> =
|
||||||
.ok_or_else(|| StashError::Decryption("no passphrase configured".into()))?;
|
OnceLock::new();
|
||||||
let identity = age::scrypt::Identity::new(passphrase);
|
let cache = CACHE.get_or_init(|| Mutex::new(None));
|
||||||
age::decrypt(&identity, ciphertext)
|
let mut guard = cache.lock().map_err(|e| {
|
||||||
|
StashError::Decryption(format!("identity cache lock poisoned: {e}").into())
|
||||||
|
})?;
|
||||||
|
if guard.is_none() {
|
||||||
|
let passphrase = load_encryption_passphrase().ok_or_else(|| {
|
||||||
|
StashError::Decryption("no passphrase configured".into())
|
||||||
|
})?;
|
||||||
|
*guard = Some(age::scrypt::Identity::new(passphrase));
|
||||||
|
}
|
||||||
|
let identity = guard
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| StashError::Decryption("identity not available".into()))?;
|
||||||
|
age::decrypt(identity, ciphertext)
|
||||||
.map_err(|e| StashError::Decryption(e.to_string().into()))
|
.map_err(|e| StashError::Decryption(e.to_string().into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
29
src/main.rs
29
src/main.rs
|
|
@ -169,13 +169,6 @@ enum DbAction {
|
||||||
ask: bool,
|
ask: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Immediately expire all entries with a TTL
|
|
||||||
Expire {
|
|
||||||
/// Ask for confirmation before expiring
|
|
||||||
#[arg(long)]
|
|
||||||
ask: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Optimize database using VACUUM
|
/// Optimize database using VACUUM
|
||||||
Vacuum,
|
Vacuum,
|
||||||
|
|
||||||
|
|
@ -413,28 +406,6 @@ fn main() -> eyre::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DbAction::Expire { ask } => {
|
|
||||||
let should_proceed = !ask
|
|
||||||
|| confirm(
|
|
||||||
"Are you sure you want to immediately expire all entries with \
|
|
||||||
a TTL?",
|
|
||||||
);
|
|
||||||
if should_proceed {
|
|
||||||
match db.expire_ttl_entries() {
|
|
||||||
Ok(0) => {
|
|
||||||
println!("no entries with a TTL to expire");
|
|
||||||
},
|
|
||||||
Ok(count) => {
|
|
||||||
println!("marked {count} entries as expired");
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("failed to expire entries: {e}");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::info!("db expire command aborted by user.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DbAction::Vacuum => {
|
DbAction::Vacuum => {
|
||||||
match db.vacuum() {
|
match db.vacuum() {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue