feat: Enhance user settings commands and data path management
- Added `fs_extra` dependency for improved file operations. - Updated `arboard` dependency to version 3.5.0. - Introduced `cmd_check_custom_data_path` to verify the status of a given path. - Implemented `cmd_validate_custom_db_path` to check if a directory is writable. - Replaced `cmd_set_custom_db_path` with `cmd_set_and_relocate_data` for setting a new data path and relocating existing data. - Replaced `cmd_remove_custom_db_path` with `cmd_revert_to_default_data_location` to revert to the default data location with optional file movement. - Refactored database path retrieval functions for clarity and efficiency. - Updated clipboard history and item services to utilize new data directory functions.
This commit is contained in:
parent
98d9673e30
commit
d92008c474
135
custom_db_location_plan.md
Normal file
135
custom_db_location_plan.md
Normal file
@ -0,0 +1,135 @@
|
||||
# Plan: Implement Custom Data Location Feature
|
||||
|
||||
This document outlines the plan to implement the feature allowing users to specify a custom location for the PasteBar application's data.
|
||||
|
||||
## 1. Goals
|
||||
|
||||
* Allow users to specify a custom parent directory for application data via the settings UI.
|
||||
* The application will create and manage a `pastebar-data` subdirectory within the user-specified location.
|
||||
* This `pastebar-data` directory will contain the database file (`pastebar-db.data`), the `clip-images` folder, and the `clipboard-images` folder.
|
||||
* Provide options to either **move** the existing data, **copy** it, or **use the new location without moving/copying**.
|
||||
* Ensure the application uses the data from the new location after a restart.
|
||||
* Handle potential errors gracefully and inform the user.
|
||||
* Update the application state and backend configuration accordingly.
|
||||
|
||||
## 2. Backend (Rust - `src-tauri`)
|
||||
|
||||
### 2.1. Configuration (`user_settings_service.rs`)
|
||||
|
||||
* The `UserConfig` struct's `custom_db_path: Option<String>` will now be repurposed to store the path to the **user-selected parent directory**. The application logic will handle appending the `/pastebar-data/` segment. This requires no change to the struct itself, only to how the path is interpreted.
|
||||
|
||||
### 2.2. Path Logic (`db.rs` and new helpers)
|
||||
|
||||
* We will introduce new helper functions to consistently resolve data paths, whether default or custom.
|
||||
* `get_data_dir() -> PathBuf`: This will be the core helper. It checks for a `custom_db_path` in the settings.
|
||||
* If present, it returns `PathBuf::from(custom_path)`.
|
||||
* If `None`, it returns the default application data directory.
|
||||
* `get_db_path()`: This function will be refactored to use `get_data_dir().join("pastebar-db.data")`.
|
||||
* `get_clip_images_dir()`: A new helper that returns `get_data_dir().join("clip-images")`.
|
||||
* `get_clipboard_images_dir()`: A new helper that returns `get_data_dir().join("clipboard-images")`.
|
||||
|
||||
### 2.3. New & Updated Tauri Commands (`user_settings_command.rs`)
|
||||
|
||||
* **`cmd_validate_custom_db_path(path: String) -> Result<bool, String>`**
|
||||
* **No change in purpose.** This command will still check if the user-selected directory is valid and writable.
|
||||
* **`cmd_check_custom_data_path(path: String) -> Result<PathStatus, String>`**
|
||||
* A new command to check the status of a selected directory. It returns one of the following statuses: `Empty`, `NotEmpty`, `IsPastebarDataAndNotEmpty`.
|
||||
* **`cmd_set_and_relocate_data(new_parent_dir_path: String, operation: String) -> Result<String, String>`** (renamed from `set_and_relocate_db`)
|
||||
* `new_parent_dir_path`: The new directory path selected by the user.
|
||||
* `operation`: Either "move", "copy", or "none".
|
||||
* **Updated Steps:**
|
||||
1. Get the source paths:
|
||||
* Current DB file path.
|
||||
* Current `clip-images` directory path.
|
||||
* Current `clipboard-images` directory path.
|
||||
2. Define the new data directory: `let new_data_dir = Path::new(&new_parent_dir_path);`
|
||||
3. Create the new data directory: `fs::create_dir_all(&new_data_dir)`.
|
||||
4. Perform file/directory operations for each item (DB file, `clip-images` dir, `clipboard-images` dir):
|
||||
* If "move": `fs::rename(source, destination)`.
|
||||
* If "copy": `fs::copy` for the file, and a recursive copy function for the directories.
|
||||
* If "none", do nothing.
|
||||
* Handle cases where source items might not exist (e.g., `clip-images` folder hasn't been created yet) by skipping them gracefully.
|
||||
5. If successful, call `user_settings_service::set_custom_db_path(&new_parent_dir_path)`.
|
||||
6. Return a success or error message.
|
||||
|
||||
* **`cmd_revert_to_default_data_location(move_files_back: bool, overwrite_default: bool) -> Result<String, String>`** (renamed and signature updated)
|
||||
* **Updated Steps:**
|
||||
1. Get the current custom data directory.
|
||||
2. Get the default application data directory.
|
||||
3. If `move_files_back` is `true`:
|
||||
* Move/copy the DB file, `clip-images` dir, and `clipboard-images` dir from the custom location back to the default location.
|
||||
* Handle the `overwrite_default` flag for each item.
|
||||
4. Call `user_settings_service::remove_custom_db_path()` to clear the setting.
|
||||
|
||||
## 3. Frontend (React)
|
||||
|
||||
* The UI has been updated to refer to "Custom Application Data Location" instead of "Custom Database Location".
|
||||
* A third radio button option, "Use new location", has been added.
|
||||
* The `handleBrowse` function now calls the `cmd_check_custom_data_path` command to analyze the selected directory and prompts the user accordingly.
|
||||
* The `settingsStore.ts` has been updated to support the "none" operation.
|
||||
|
||||
## 4. User Interaction Flow (Mermaid Diagram)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph User Flow
|
||||
A[User navigates to User Preferences] --> B{Custom Data Path Set?};
|
||||
B -- Yes --> C[Display Current Custom Path];
|
||||
B -- No --> D[Display Current Path: Default];
|
||||
|
||||
C --> E[Show "Revert to Default" Button];
|
||||
D --> F[User Selects New Parent Directory];
|
||||
F --> G{Path Status?};
|
||||
G -- Empty --> H[Set Path];
|
||||
G -- Not Empty --> I{Confirm "pastebar-data" subfolder};
|
||||
I -- Yes --> J[Append "pastebar-data" to path];
|
||||
J --> H;
|
||||
I -- No --> H;
|
||||
G -- Is 'pastebar-data' and Not Empty --> K[Alert user existing data will be used];
|
||||
K --> H;
|
||||
H --> L[User Selects Operation: Move/Copy/None];
|
||||
L --> M[User Clicks "Apply and Restart"];
|
||||
end
|
||||
|
||||
subgraph Backend Logic
|
||||
M --> N[Frontend calls `cmd_set_and_relocate_data`];
|
||||
N -- Success --> O[1. Create new data dir if needed];
|
||||
O --> P[2. Move/Copy/Skip data];
|
||||
P --> Q[3. Update `custom_db_path` in settings];
|
||||
Q --> R[Show Success Toast & Relaunch App];
|
||||
N -- Error --> S[Show Error Toast];
|
||||
|
||||
E --> T[User Clicks "Revert"];
|
||||
T --> U[Frontend calls `cmd_revert_to_default_data_location`];
|
||||
U -- Success --> V[Move/Copy data back to default app dir & clear setting];
|
||||
V --> W[Show Success Toast & Relaunch App];
|
||||
U -- Error --> X[Show Error Toast];
|
||||
end
|
||||
|
||||
D -- "Browse..." --> F;
|
||||
```
|
||||
|
||||
## 5. Implementation Summary
|
||||
|
||||
The following changes have been implemented:
|
||||
|
||||
* **`packages/pastebar-app-ui/src/pages/settings/UserPreferences.tsx`**:
|
||||
* Renamed "Custom Database Location" to "Custom Application Data Location".
|
||||
* Added a third radio button for the "Use new location" option.
|
||||
* Updated the `handleBrowse` function to call the new `cmd_check_custom_data_path` command and handle the different path statuses with user prompts.
|
||||
* **`packages/pastebar-app-ui/src/store/settingsStore.ts`**:
|
||||
* Updated the `applyCustomDbPath` function to accept the "none" operation.
|
||||
* Updated the `revertToDefaultDbPath` function to call the renamed backend command.
|
||||
* **`src-tauri/src/commands/user_settings_command.rs`**:
|
||||
* Added the `cmd_check_custom_data_path` command.
|
||||
* Renamed `cmd_set_and_relocate_db` to `cmd_set_and_relocate_data` and updated its logic to handle the "none" operation and the new data directory structure.
|
||||
* Renamed `cmd_revert_to_default_db_location` to `cmd_revert_to_default_data_location` and updated its logic.
|
||||
* **`src-tauri/src/db.rs`**:
|
||||
* Refactored the `get_data_dir` function to no longer automatically append `pastebar-data`.
|
||||
* Added `get_clip_images_dir` and `get_clipboard_images_dir` helper functions.
|
||||
* **`src-tauri/src/main.rs`**:
|
||||
* Registered the new and renamed commands in the `invoke_handler`.
|
||||
* **`src-tauri/Cargo.toml`**:
|
||||
* Added the `fs_extra` dependency for recursive directory copying.
|
||||
* **`src-tauri/src/services/items_service.rs`** and **`src-tauri/src/services/history_service.rs`**:
|
||||
* Updated to use the new `get_clip_images_dir` and `get_clipboard_images_dir` helper functions.
|
@ -107,6 +107,11 @@ function App() {
|
||||
|
||||
settingsStore.initSettings({
|
||||
appDataDir: import.meta.env.TAURI_DEBUG ? appDevDataDir : appDataDir,
|
||||
// Initialize new DB path settings for type conformity; actual value loaded by loadInitialCustomDbPath
|
||||
customDbPath: null,
|
||||
isCustomDbPathValid: null,
|
||||
customDbPathError: null,
|
||||
dbRelocationInProgress: false,
|
||||
appLastUpdateVersion: settings.appLastUpdateVersion?.valueText,
|
||||
appLastUpdateDate: settings.appLastUpdateDate?.valueText,
|
||||
isHideMacOSDockIcon: settings.isHideMacOSDockIcon?.valueBool,
|
||||
@ -187,6 +192,8 @@ function App() {
|
||||
settingsStore.initConstants({
|
||||
APP_DETECT_LANGUAGES_SUPPORTED: appDetectLanguageSupport,
|
||||
})
|
||||
// Load the actual custom DB path after basic settings are initialized
|
||||
settingsStore.loadInitialCustomDbPath()
|
||||
type().then(osType => {
|
||||
if (osType === 'Windows_NT' && settings.copyPasteDelay?.valueInt === 0) {
|
||||
settingsStore.updateSetting('copyPasteDelay', 2)
|
||||
|
@ -24,6 +24,7 @@ Are you sure you want to delete?: Are you sure you want to delete?
|
||||
Are you sure?: Are you sure?
|
||||
Attach History Window: Attach History Window
|
||||
Back: Back
|
||||
Browse...: Browse...
|
||||
Build on {{buildDate}}: Build on {{buildDate}}
|
||||
Cancel: Cancel
|
||||
Cancel Reset: Cancel Reset
|
||||
|
@ -12,6 +12,8 @@ Application UI Color Theme: Application UI Color Theme
|
||||
Application UI Fonts Scale: Application UI Fonts Scale
|
||||
Application UI Language: Application UI Language
|
||||
Applications listed below will not have their copy to clipboard action captured in clipboard history. Case insensitive.: Applications listed below will not have their copy to clipboard action captured in clipboard history. Case insensitive.
|
||||
Apply and Restart: Apply and Restart
|
||||
Are you sure you want to revert to the default database location? The application will restart.: Are you sure you want to revert to the default database location? The application will restart.
|
||||
Auto Disable History Capture when Screen Unlocked: Auto Disable History Capture when Screen Unlocked
|
||||
Auto Lock Application Screen on User Inactivity: Auto Lock Application Screen on User Inactivity
|
||||
Auto Lock Screen on User Inactivity: Auto Lock Screen on User Inactivity
|
||||
@ -35,20 +37,28 @@ Change the application UI language: Change the application UI language
|
||||
Change the application user interface color theme: Change the application user interface color theme
|
||||
Change the application user interface font size scale: Change the application user interface font size scale
|
||||
Change the application user interface language: Change the application user interface language
|
||||
Changing the database location requires an application restart to take effect.: Changing the database location requires an application restart to take effect.
|
||||
Clip Notes Popup Maximum Dimensions: Clip Notes Popup Maximum Dimensions
|
||||
Clipboard History Settings: Clipboard History Settings
|
||||
'Complete details:': 'Complete details:'
|
||||
Configure settings to automatically delete clipboard history items after a specified duration.: Configure settings to automatically delete clipboard history items after a specified duration.
|
||||
Copy data: Copy data
|
||||
Copy database file: Copy database file
|
||||
Create a preview card on link hover in the clipboard history. This allows you to preview the link before opening or pasting it.: Create a preview card on link hover in the clipboard history. This allows you to preview the link before opening or pasting it.
|
||||
Create an unlimited number of collections to organize your clips and menus.: Create an unlimited number of collections to organize your clips and menus.
|
||||
Current database location: Current database location
|
||||
Custom: Custom
|
||||
Custom Application Data Location: Custom Application Data Location
|
||||
Custom Database Location: Custom Database Location
|
||||
Custom themes: Custom themes
|
||||
Decrease UI Font Size: Decrease UI Font Size
|
||||
Default: Default
|
||||
? Display clipboard history capture toggle on the locked application screen. This allows you to control history capture settings directly from the lock screen.
|
||||
: Display clipboard history capture toggle on the locked application screen. This allows you to control history capture settings directly from the lock screen.
|
||||
Display disabled collections name on the navigation bar collections menu: Display disabled collections name on the navigation bar collections menu
|
||||
Display disabled collections name on the navigation bar under collections menu: Display disabled collections name on the navigation bar under collections menu
|
||||
Display full name of selected collection on the navigation bar: Display full name of selected collection on the navigation bar
|
||||
Do you want to attempt to move the database file from "{{customPath}}" back to the default location?: Do you want to attempt to move the database file from "{{customPath}}" back to the default location?
|
||||
Drag and drop to prioritize languages for detection. The higher a language is in the list, the higher its detection priority.: Drag and drop to prioritize languages for detection. The higher a language is in the list, the higher its detection priority.
|
||||
Email: Email
|
||||
Email is not valid: Email is not valid
|
||||
@ -66,6 +76,7 @@ Enable programming language detection: Enable programming language detection
|
||||
Enable screen unlock requirement on app launch for enhanced security, safeguarding data from unauthorized access.: Enable screen unlock requirement on app launch for enhanced security, safeguarding data from unauthorized access.
|
||||
Enhance security by automatically locking the application screen after a set period of user inactivity.: Enhance security by automatically locking the application screen after a set period of user inactivity.
|
||||
Enter Passcode length: Enter Passcode length
|
||||
Enter new directory path or leave empty for default on next revert: Enter new directory path or leave empty for default on next revert
|
||||
Enter recovery password to reset passcode.: Enter recovery password to reset passcode.
|
||||
Enter your <strong>{{screenLockPassCodeLength}} digits</strong> passcode: Enter your <strong>{{screenLockPassCodeLength}} digits</strong> passcode
|
||||
Entered Passcode is invalid: Entered Passcode is invalid
|
||||
@ -73,12 +84,15 @@ Excluded Apps List: Excluded Apps List
|
||||
Execute Web Requests: Execute Web Requests
|
||||
Execute terminal or shell commands directly from PasteBar clip and copy the results to the clipboard.: Execute terminal or shell commands directly from PasteBar clip and copy the results to the clipboard.
|
||||
'Expires:': 'Expires:'
|
||||
Failed to revert to default database location.: Failed to revert to default database location.
|
||||
Forgot Passcode ? Enter your recovery password to reset the passcode.: Forgot Passcode ? Enter your recovery password to reset the passcode.
|
||||
Forgot passcode ?: Forgot passcode ?
|
||||
Forgot?: Forgot?
|
||||
Forgot? Reset using Password: Forgot? Reset using Password
|
||||
Get priority email support from us to resolve any issues or questions you may have about PasteBar.: Get priority email support from us to resolve any issues or questions you may have about PasteBar.
|
||||
'Hint: {{screenLockRecoveryPasswordMasked}}': 'Hint: {{screenLockRecoveryPasswordMasked}}'
|
||||
? If a database file already exists at the default location, do you want to overwrite it? Choosing "Cancel" will skip moving the file if an existing file is found.
|
||||
: If a database file already exists at the default location, do you want to overwrite it? Choosing "Cancel" will skip moving the file if an existing file is found.
|
||||
Incorrect passcode.: Incorrect passcode.
|
||||
Increase UI Font Size: Increase UI Font Size
|
||||
Issued: Issued
|
||||
@ -95,8 +109,13 @@ Medium: Medium
|
||||
Minimal 4 digits: Minimal 4 digits
|
||||
Minimize Window: Minimize Window
|
||||
Minimum number of lines to trigger detection: Minimum number of lines to trigger detection
|
||||
Move data: Move data
|
||||
Move database file: Move database file
|
||||
Name: Name
|
||||
New Data Directory Path: New Data Directory Path
|
||||
New Database Directory Path: New Database Directory Path
|
||||
Open Security Settings: Open Security Settings
|
||||
Operation when applying new path: Operation when applying new path
|
||||
Passcode digits remaining: Passcode digits remaining
|
||||
Passcode is locked.: Passcode is locked.
|
||||
Passcode is not set: Passcode is not set
|
||||
@ -132,12 +151,15 @@ Require Screen Unlock at Application Start: Require Screen Unlock at Application
|
||||
? Require screen unlock at application launch to enhance security. This setting ensures that only authorized users can access the application, protecting your data from unauthorized access right from the start.
|
||||
: Require screen unlock at application launch to enhance security. This setting ensures that only authorized users can access the application, protecting your data from unauthorized access right from the start.
|
||||
Reset Font Size: Reset Font Size
|
||||
Revert to Default and Restart: Revert to Default and Restart
|
||||
Run Terminal or Shell Commands: Run Terminal or Shell Commands
|
||||
Scrape and parse websites or API responses using built-in web scraping tools and copy the extracted data to the clipboard.: Scrape and parse websites or API responses using built-in web scraping tools and copy the extracted data to the clipboard.
|
||||
? 'Scrape and parse websites or API responses using built-in web scraping tools and copy the extracted data to the clipboard. '
|
||||
: 'Scrape and parse websites or API responses using built-in web scraping tools and copy the extracted data to the clipboard. '
|
||||
Security: Security
|
||||
Security Settings: Security Settings
|
||||
Select Data Directory: Select Data Directory
|
||||
Select Database Directory: Select Database Directory
|
||||
Send HTTP requests to web APIs or services and copy the response data to the clipboard.: Send HTTP requests to web APIs or services and copy the response data to the clipboard.
|
||||
Sensitive words or sentences listed below will automatically be masked if found in the copied text. Case insensitive.: Sensitive words or sentences listed below will automatically be masked if found in the copied text. Case insensitive.
|
||||
Set a passcode to unlock the locked screen and protect your data from unauthorized access.: Set a passcode to unlock the locked screen and protect your data from unauthorized access.
|
||||
@ -158,6 +180,8 @@ Swap Panels Layout: Swap Panels Layout
|
||||
Switch the layout position of panels in Clipboard History and Paste Menu views: Switch the layout position of panels in Clipboard History and Paste Menu views
|
||||
Thank you again for using PasteBar.: Thank you again for using PasteBar.
|
||||
Thank you for testing! 🙌: Thank you for testing! 🙌
|
||||
The selected folder is not empty. Do you want to create a "pastebar-data" subfolder to store the data?: The selected folder is not empty. Do you want to create a "pastebar-data" subfolder to store the data?
|
||||
This folder already contains PasteBar data. The application will use this existing data after restart.: This folder already contains PasteBar data. The application will use this existing data after restart.
|
||||
? This option lets you control the display and timing of hover notes on clips. You can choose to show notes instantly or with a delay to prevent unintended popups.
|
||||
: This option lets you control the display and timing of hover notes on clips. You can choose to show notes instantly or with a delay to prevent unintended popups.
|
||||
? This option lets you customize the maximum width and height of the popup that displays clip notes, ensuring it fits comfortably within your desired size.
|
||||
@ -177,6 +201,7 @@ Unlimited Collections: Unlimited Collections
|
||||
Unlimited Tabs per Collection: Unlimited Tabs per Collection
|
||||
Unlimited paste history: Unlimited paste history
|
||||
Use Password: Use Password
|
||||
Use new location: Use new location
|
||||
User Preferences: User Preferences
|
||||
Web Scraping and Parsing: Web Scraping and Parsing
|
||||
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { dialog, invoke } from '@tauri-apps/api'
|
||||
import { join } from '@tauri-apps/api/path'
|
||||
import i18n from '~/locales'
|
||||
import { LANGUAGES } from '~/locales/languges'
|
||||
import {
|
||||
clipNotesDelays,
|
||||
clipNotesSizes,
|
||||
fontSizeIncrements,
|
||||
settingsStore, // Import the Zustand store instance for getState()
|
||||
settingsStoreAtom,
|
||||
themeStoreAtom,
|
||||
uiStoreAtom,
|
||||
@ -39,6 +41,335 @@ import {
|
||||
|
||||
import md from '~/store/example.md?raw'
|
||||
|
||||
// Helper component for Custom Database Location settings
|
||||
function CustomDatabaseLocationSettings() {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
customDbPath,
|
||||
// isCustomDbPathValid is checked via settingsStore.getState() after validation
|
||||
customDbPathError: storeCustomDbPathError, // Renamed to avoid conflict with local error state
|
||||
dbRelocationInProgress,
|
||||
validateCustomDbPath,
|
||||
applyCustomDbPath,
|
||||
revertToDefaultDbPath,
|
||||
relaunchApp,
|
||||
} = useAtomValue(settingsStoreAtom)
|
||||
|
||||
const [newDbPathInput, setNewDbPathInput] = useState(customDbPath || '')
|
||||
const [dbOperation, setDbOperation] = useState<'move' | 'copy' | 'none'>('copy')
|
||||
|
||||
const [isApplying, setIsApplying] = useState(false)
|
||||
const [isReverting, setIsReverting] = useState(false)
|
||||
const [operationError, setOperationError] = useState<string | null>(null)
|
||||
const [validationError, setValidationError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setNewDbPathInput(customDbPath || '')
|
||||
setValidationError(null) // Clear local validation error when customDbPath changes
|
||||
}, [customDbPath])
|
||||
|
||||
// Effect to react to validation errors from the store
|
||||
useEffect(() => {
|
||||
const state = settingsStore.getState()
|
||||
if (newDbPathInput && storeCustomDbPathError && !state.isCustomDbPathValid) {
|
||||
setValidationError(storeCustomDbPathError)
|
||||
} else if (state.isCustomDbPathValid) {
|
||||
setValidationError(null)
|
||||
}
|
||||
// Intentionally not depending on state.isCustomDbPathValid directly to avoid loop with onBlur validation
|
||||
}, [storeCustomDbPathError, newDbPathInput])
|
||||
|
||||
const handleBrowse = async () => {
|
||||
setOperationError(null)
|
||||
setValidationError(null)
|
||||
try {
|
||||
const selected = await dialog.open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: t('Select Data Directory', { ns: 'settings' }),
|
||||
})
|
||||
if (typeof selected === 'string') {
|
||||
const status: any = await invoke('cmd_check_custom_data_path', {
|
||||
pathStr: selected,
|
||||
})
|
||||
let finalPath = selected
|
||||
if (status === 'NotEmpty') {
|
||||
const confirmSubfolder = window.confirm(
|
||||
t(
|
||||
'The selected folder is not empty. Do you want to create a "pastebar-data" subfolder to store the data?',
|
||||
{ ns: 'settings' }
|
||||
)
|
||||
)
|
||||
if (confirmSubfolder) {
|
||||
finalPath = await join(selected, 'pastebar-data')
|
||||
}
|
||||
} else if (status === 'IsPastebarDataAndNotEmpty') {
|
||||
window.alert(
|
||||
t(
|
||||
'This folder already contains PasteBar data. The application will use this existing data after restart.',
|
||||
{ ns: 'settings' }
|
||||
)
|
||||
)
|
||||
}
|
||||
setNewDbPathInput(finalPath)
|
||||
await validateCustomDbPath(finalPath)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling directory selection:', error)
|
||||
setOperationError(
|
||||
t('An error occurred during directory processing.', { ns: 'settings' })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleApply = async () => {
|
||||
if (!newDbPathInput || newDbPathInput === customDbPath) {
|
||||
setOperationError(
|
||||
t('Please select a new directory different from the current one.', {
|
||||
ns: 'settings',
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
setIsApplying(true)
|
||||
setOperationError(null)
|
||||
setValidationError(null)
|
||||
|
||||
await validateCustomDbPath(newDbPathInput)
|
||||
const currentStoreState = settingsStore.getState()
|
||||
|
||||
if (!currentStoreState.isCustomDbPathValid) {
|
||||
setValidationError(
|
||||
currentStoreState.customDbPathError ||
|
||||
t('Invalid directory selected.', { ns: 'settings' })
|
||||
)
|
||||
setIsApplying(false)
|
||||
return
|
||||
}
|
||||
|
||||
const confirmed = window.confirm(
|
||||
t(
|
||||
'Are you sure you want to {{operation}} the database to "{{path}}"? The application will restart.',
|
||||
{
|
||||
ns: 'settings',
|
||||
operation: t(dbOperation, { ns: 'settings' }),
|
||||
path: newDbPathInput,
|
||||
}
|
||||
)
|
||||
)
|
||||
if (confirmed) {
|
||||
try {
|
||||
await applyCustomDbPath(newDbPathInput, dbOperation)
|
||||
// Consider using a toast notification here
|
||||
relaunchApp()
|
||||
} catch (error: any) {
|
||||
setOperationError(
|
||||
error.message ||
|
||||
t('Failed to apply custom database location.', { ns: 'settings' })
|
||||
)
|
||||
} finally {
|
||||
setIsApplying(false)
|
||||
}
|
||||
} else {
|
||||
setIsApplying(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRevert = async () => {
|
||||
setOperationError(null)
|
||||
setValidationError(null)
|
||||
const confirmedInitial = window.confirm(
|
||||
t(
|
||||
'Are you sure you want to revert to the default database location? The application will restart.',
|
||||
{ ns: 'settings' }
|
||||
)
|
||||
)
|
||||
if (!confirmedInitial) return
|
||||
|
||||
let moveFileConfirmed = false
|
||||
let overwriteConfirmed = false // Default to false
|
||||
|
||||
if (customDbPath) {
|
||||
// Only ask about moving if there IS a custom path currently set
|
||||
moveFileConfirmed = window.confirm(
|
||||
t(
|
||||
'Do you want to attempt to move the database file from "{{customPath}}" back to the default location?',
|
||||
{ ns: 'settings', customPath: customDbPath }
|
||||
)
|
||||
)
|
||||
if (moveFileConfirmed) {
|
||||
// Only ask for overwrite if they chose to move the file
|
||||
overwriteConfirmed = window.confirm(
|
||||
t(
|
||||
'If a database file already exists at the default location, do you want to overwrite it? Choosing "Cancel" will skip moving the file if an existing file is found.',
|
||||
{ ns: 'settings' }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setIsReverting(true)
|
||||
try {
|
||||
await revertToDefaultDbPath(moveFileConfirmed, overwriteConfirmed)
|
||||
// Consider using a toast notification here
|
||||
relaunchApp()
|
||||
} catch (error: any) {
|
||||
setOperationError(
|
||||
error.message ||
|
||||
t('Failed to revert to default database location.', { ns: 'settings' })
|
||||
)
|
||||
} finally {
|
||||
setIsReverting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const isLoading = dbRelocationInProgress || isApplying || isReverting
|
||||
const currentPathDisplay = customDbPath || t('Default', { ns: 'settings' })
|
||||
const isPathUnchanged = newDbPathInput === customDbPath
|
||||
|
||||
return (
|
||||
<Box className="animate-in fade-in max-w-xl mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{t('Custom Application Data Location', { ns: 'settings' })}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Text className="text-sm text-muted-foreground">
|
||||
{t('Current database location', { ns: 'settings' })}:{' '}
|
||||
<span className="font-semibold">{currentPathDisplay}</span>
|
||||
</Text>
|
||||
|
||||
<InputField
|
||||
label={t('New Data Directory Path', { ns: 'settings' })}
|
||||
value={newDbPathInput}
|
||||
onChange={e => {
|
||||
setNewDbPathInput(e.target.value)
|
||||
setOperationError(null)
|
||||
setValidationError(null) // Clear validation error on input change
|
||||
}}
|
||||
onBlur={async () => {
|
||||
if (newDbPathInput && newDbPathInput !== customDbPath) {
|
||||
await validateCustomDbPath(newDbPathInput)
|
||||
} else if (!newDbPathInput && customDbPath) {
|
||||
// if input is cleared but a custom path was set
|
||||
settingsStore.setState({
|
||||
customDbPathError: null,
|
||||
isCustomDbPathValid: null,
|
||||
})
|
||||
setValidationError(null)
|
||||
} else if (newDbPathInput === customDbPath) {
|
||||
settingsStore.setState({
|
||||
customDbPathError: null,
|
||||
isCustomDbPathValid: null,
|
||||
})
|
||||
setValidationError(null)
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
placeholder={t(
|
||||
'Enter new directory path or leave empty for default on next revert',
|
||||
{ ns: 'settings' }
|
||||
)}
|
||||
/>
|
||||
{validationError && (
|
||||
<Text className="text-sm text-red-500">{validationError}</Text>
|
||||
)}
|
||||
|
||||
<Button onClick={handleBrowse} disabled={isLoading} variant="outline">
|
||||
{dbRelocationInProgress && !isApplying && !isReverting ? (
|
||||
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
{t('Browse...', { ns: 'common' })}
|
||||
</Button>
|
||||
|
||||
<Flex className="items-center space-x-4">
|
||||
<Text className="text-sm">
|
||||
{t('Operation when applying new path', { ns: 'settings' })}:
|
||||
</Text>
|
||||
<label className="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="dbOperation"
|
||||
value="copy"
|
||||
checked={dbOperation === 'copy'}
|
||||
onChange={() => setDbOperation('copy')}
|
||||
disabled={isLoading}
|
||||
className="form-radio accent-primary"
|
||||
/>
|
||||
<TextNormal size="sm">{t('Copy data', { ns: 'settings' })}</TextNormal>
|
||||
</label>
|
||||
<label className="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="dbOperation"
|
||||
value="move"
|
||||
checked={dbOperation === 'move'}
|
||||
onChange={() => setDbOperation('move')}
|
||||
disabled={isLoading}
|
||||
className="form-radio accent-primary"
|
||||
/>
|
||||
<TextNormal size="sm">{t('Move data', { ns: 'settings' })}</TextNormal>
|
||||
</label>
|
||||
<label className="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="dbOperation"
|
||||
value="none"
|
||||
checked={dbOperation === 'none'}
|
||||
onChange={() => setDbOperation('none')}
|
||||
disabled={isLoading}
|
||||
className="form-radio accent-primary"
|
||||
/>
|
||||
<TextNormal size="sm">
|
||||
{t('Use new location', { ns: 'settings' })}
|
||||
</TextNormal>
|
||||
</label>
|
||||
</Flex>
|
||||
|
||||
{operationError && (
|
||||
<Text className="text-sm text-red-500">{operationError}</Text>
|
||||
)}
|
||||
|
||||
<Flex className="space-x-2">
|
||||
<Button
|
||||
onClick={handleApply}
|
||||
disabled={
|
||||
isLoading || !newDbPathInput || isPathUnchanged || !!validationError
|
||||
}
|
||||
>
|
||||
{isApplying ? (
|
||||
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
{t('Apply and Restart', { ns: 'settings' })}
|
||||
</Button>
|
||||
{customDbPath && ( // Only show revert button if a custom path is currently set
|
||||
<Button
|
||||
onClick={handleRevert}
|
||||
disabled={isLoading}
|
||||
variant="secondary" // Base variant
|
||||
className="bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 text-white dark:text-slate-100" // Destructive-like styling
|
||||
>
|
||||
{isReverting ? (
|
||||
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
{t('Revert to Default and Restart', { ns: 'settings' })}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<Text className="text-xs text-muted-foreground pt-2">
|
||||
{t(
|
||||
'Changing the database location requires an application restart to take effect.',
|
||||
{ ns: 'settings' }
|
||||
)}
|
||||
</Text>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default function UserPreferences() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -69,12 +400,16 @@ export default function UserPreferences() {
|
||||
hotKeysShowHideQuickPasteWindow,
|
||||
setHotKeysShowHideMainAppWindow,
|
||||
setHotKeysShowHideQuickPasteWindow,
|
||||
// Custom DB Path states and actions (customDbPath, isCustomDbPathValid, etc.)
|
||||
// are now handled in the CustomDatabaseLocationSettings component.
|
||||
// relaunchApp is also used there.
|
||||
} = useAtomValue(settingsStoreAtom)
|
||||
|
||||
const { setFontSize, fontSize, setIsSwapPanels, isSwapPanels, returnRoute, isMacOSX } =
|
||||
useAtomValue(uiStoreAtom)
|
||||
|
||||
const [isAutoStartEnabled, setIsAutoStartEnabled] = useState(false)
|
||||
// Local states for DB path input, operation, and confirmations have been moved to CustomDatabaseLocationSettings
|
||||
|
||||
const { setTheme, theme } = useTheme()
|
||||
const { mode, setMode, themeDark } = useAtomValue(themeStoreAtom)
|
||||
@ -83,7 +418,7 @@ export default function UserPreferences() {
|
||||
if (theme !== mode) {
|
||||
setMode(theme)
|
||||
}
|
||||
}, [theme])
|
||||
}, [theme, mode, setMode]) // Added mode and setMode to dependency array
|
||||
|
||||
useEffect(() => {
|
||||
invoke('is_autostart_enabled').then(isEnabled => {
|
||||
@ -107,6 +442,8 @@ export default function UserPreferences() {
|
||||
setQuickPasteHotkey(hotKeysShowHideQuickPasteWindow)
|
||||
}
|
||||
}, [hotKeysShowHideMainAppWindow, hotKeysShowHideQuickPasteWindow])
|
||||
// Removed mainAppHotkey, quickPasteHotkey from local state dependencies in the original thought process,
|
||||
// as they are set inside this effect. The effect correctly depends on props.
|
||||
|
||||
const handleKeyDown = (
|
||||
event: KeyboardEvent | React.KeyboardEvent<HTMLInputElement>,
|
||||
@ -274,6 +611,10 @@ export default function UserPreferences() {
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* ------------- Custom Database Location Settings Card ------------- */}
|
||||
<CustomDatabaseLocationSettings />
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
|
||||
<Box className="animate-in fade-in max-w-xl mt-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col items-start justify-between space-y-0 pb-1">
|
||||
|
@ -26,7 +26,11 @@ import {
|
||||
type Settings = {
|
||||
appLastUpdateVersion: string
|
||||
appLastUpdateDate: string
|
||||
appDataDir: string
|
||||
appDataDir: string // This might be the old customDbPath or a general app data dir. We'll add a specific one.
|
||||
customDbPath: string | null // Path to the custom database directory
|
||||
isCustomDbPathValid: boolean | null // Validation status of the entered customDbPath
|
||||
customDbPathError: string | null // Error message if validation fails or operation fails
|
||||
dbRelocationInProgress: boolean // True if a DB move/copy/revert operation is ongoing
|
||||
isAppReady: boolean
|
||||
isClipNotesHoverCardsEnabled: boolean
|
||||
clipNotesHoverCardsDelayMS: number
|
||||
@ -88,6 +92,14 @@ type Constants = {
|
||||
}
|
||||
|
||||
export interface SettingsStoreState {
|
||||
setCustomDbPath: (path: string | null) => void
|
||||
validateCustomDbPath: (path: string) => Promise<void>
|
||||
applyCustomDbPath: (
|
||||
newPath: string,
|
||||
operation: 'move' | 'copy' | 'none'
|
||||
) => Promise<string>
|
||||
revertToDefaultDbPath: (moveFile: boolean, overwrite: boolean) => Promise<string>
|
||||
loadInitialCustomDbPath: () => Promise<void>
|
||||
setIsHistoryEnabled: (isHistoryEnabled: boolean) => void
|
||||
setIsHistoryAutoUpdateOnCaputureEnabled: (
|
||||
isHistoryAutoUpdateOnCaputureEnabled: boolean
|
||||
@ -120,7 +132,7 @@ export interface SettingsStoreState {
|
||||
setAppToursCompletedList: (words: string[]) => void
|
||||
setAppToursSkippedList: (words: string[]) => void
|
||||
setHistoryDetectLanguagesPrioritizedList: (words: string[]) => void
|
||||
setAppDataDir: (appDataDir: string) => void
|
||||
setAppDataDir: (appDataDir: string) => void // Keep if used for other general app data
|
||||
setIsAutoCloseOnCopyPaste: (isEnabled: boolean) => void
|
||||
setClipNotesHoverCardsDelayMS: (delay: number) => void
|
||||
setClipNotesMaxWidth: (width: number) => void
|
||||
@ -175,7 +187,11 @@ const initialState: SettingsStoreState & Settings = {
|
||||
appLastUpdateVersion: '0.0.1',
|
||||
appLastUpdateDate: '',
|
||||
isAppReady: false,
|
||||
appDataDir: '',
|
||||
appDataDir: '', // Default app data dir if needed for other things
|
||||
customDbPath: null,
|
||||
isCustomDbPathValid: null,
|
||||
customDbPathError: null,
|
||||
dbRelocationInProgress: false,
|
||||
isHistoryEnabled: true,
|
||||
isFirstRun: true,
|
||||
historyDetectLanguagesEnabledList: [],
|
||||
@ -287,7 +303,12 @@ const initialState: SettingsStoreState & Settings = {
|
||||
setClipTextMinLength: () => {},
|
||||
setClipTextMaxLength: () => {},
|
||||
initConstants: () => {},
|
||||
setAppDataDir: () => {},
|
||||
setAppDataDir: () => {}, // Keep if used for other general app data
|
||||
setCustomDbPath: () => {},
|
||||
validateCustomDbPath: async () => {},
|
||||
applyCustomDbPath: async () => '',
|
||||
revertToDefaultDbPath: async () => '',
|
||||
loadInitialCustomDbPath: async () => {},
|
||||
updateSetting: () => {},
|
||||
setIsFirstRun: () => {},
|
||||
setAppLastUpdateVersion: () => {},
|
||||
@ -738,10 +759,80 @@ export const settingsStore = createStore<SettingsStoreState & Settings>()((set,
|
||||
availableVersionDateISO.value = null
|
||||
},
|
||||
initConstants: (CONST: Constants) => set(() => ({ CONST })),
|
||||
setAppDataDir: (appDataDir: string) =>
|
||||
setAppDataDir: (
|
||||
appDataDir: string // Keep if used for other general app data
|
||||
) =>
|
||||
set(() => ({
|
||||
appDataDir,
|
||||
})),
|
||||
// Actions for custom DB path
|
||||
setCustomDbPath: (path: string | null) =>
|
||||
set({ customDbPath: path, isCustomDbPathValid: null, customDbPathError: null }),
|
||||
loadInitialCustomDbPath: async () => {
|
||||
try {
|
||||
const path = await invoke('cmd_get_custom_db_path')
|
||||
set({ customDbPath: path as string | null })
|
||||
} catch (error) {
|
||||
console.error('Failed to load initial custom DB path:', error)
|
||||
set({ customDbPathError: 'Failed to load custom DB path setting.' })
|
||||
}
|
||||
},
|
||||
validateCustomDbPath: async (path: string) => {
|
||||
set({
|
||||
dbRelocationInProgress: true,
|
||||
customDbPathError: null,
|
||||
isCustomDbPathValid: null,
|
||||
})
|
||||
try {
|
||||
await invoke('cmd_validate_custom_db_path', { pathStr: path })
|
||||
set({ isCustomDbPathValid: true, dbRelocationInProgress: false })
|
||||
} catch (error) {
|
||||
console.error('Custom DB path validation failed:', error)
|
||||
set({
|
||||
isCustomDbPathValid: false,
|
||||
customDbPathError: error as string,
|
||||
dbRelocationInProgress: false,
|
||||
})
|
||||
}
|
||||
},
|
||||
applyCustomDbPath: async (newPath: string, operation: 'move' | 'copy' | 'none') => {
|
||||
set({ dbRelocationInProgress: true, customDbPathError: null })
|
||||
try {
|
||||
const message = await invoke('cmd_set_and_relocate_data', {
|
||||
newParentDirPath: newPath,
|
||||
operation,
|
||||
})
|
||||
set({
|
||||
customDbPath: newPath,
|
||||
isCustomDbPathValid: true,
|
||||
dbRelocationInProgress: false,
|
||||
})
|
||||
return message as string
|
||||
} catch (error) {
|
||||
console.error('Failed to apply custom DB path:', error)
|
||||
set({ customDbPathError: error as string, dbRelocationInProgress: false })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
revertToDefaultDbPath: async (moveFile: boolean, overwrite: boolean) => {
|
||||
set({ dbRelocationInProgress: true, customDbPathError: null })
|
||||
try {
|
||||
const message = await invoke('cmd_revert_to_default_data_location', {
|
||||
moveFilesBack: moveFile,
|
||||
overwriteDefault: overwrite,
|
||||
})
|
||||
set({
|
||||
customDbPath: null,
|
||||
isCustomDbPathValid: null,
|
||||
dbRelocationInProgress: false,
|
||||
})
|
||||
return message as string
|
||||
} catch (error) {
|
||||
console.error('Failed to revert to default DB path:', error)
|
||||
set({ customDbPathError: error as string, dbRelocationInProgress: false })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
setAppLastUpdateVersion: (appLastUpdateVersion: string) => {
|
||||
return get().updateSetting('appLastUpdateVersion', appLastUpdateVersion)
|
||||
},
|
||||
|
@ -1 +1,2 @@
|
||||
custom_db_path: "/Users/kurdin/"
|
||||
custom_db_path: V:\iCloudDrive\AppSyncData
|
||||
data: {}
|
||||
|
2296
src-tauri/Cargo.lock
generated
2296
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,7 @@ winapi = { version = "0.3", features = ["winuser", "windef"] }
|
||||
winreg = "0.52.0"
|
||||
|
||||
[dependencies]
|
||||
fs_extra = "1.3.0"
|
||||
fns = "0"
|
||||
mouse_position = "0.1.4"
|
||||
keyring = "2.3.2"
|
||||
@ -65,7 +66,7 @@ chrono = { version = "0.4.24", features = ["serde"] }
|
||||
uuid = "1.3.1"
|
||||
once_cell = "1.7.0"
|
||||
thiserror = "1.0"
|
||||
arboard = "3.2.1"
|
||||
arboard = "3.5.0"
|
||||
image = "0.24.9"
|
||||
tempfile = "3"
|
||||
base64 = "0.22.0"
|
||||
|
@ -1,11 +1,47 @@
|
||||
use serde_yaml::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use tauri::command;
|
||||
|
||||
use crate::services::user_settings_service::{
|
||||
get_all_settings, get_custom_db_path, get_setting, remove_custom_db_path, remove_setting,
|
||||
set_custom_db_path, set_setting,
|
||||
use crate::db::{
|
||||
get_clip_images_dir, get_clipboard_images_dir, get_data_dir, get_db_path, get_default_data_dir,
|
||||
get_default_db_path_string,
|
||||
};
|
||||
use crate::services::user_settings_service::{
|
||||
self as user_settings_service, get_all_settings, get_custom_db_path, get_setting,
|
||||
remove_custom_db_path, remove_setting, set_custom_db_path, set_setting,
|
||||
};
|
||||
use fs_extra::dir::{copy, CopyOptions};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub enum PathStatus {
|
||||
Empty,
|
||||
NotEmpty,
|
||||
IsPastebarDataAndNotEmpty,
|
||||
}
|
||||
|
||||
/// Checks the status of a given path.
|
||||
#[command]
|
||||
pub fn cmd_check_custom_data_path(path_str: String) -> Result<PathStatus, String> {
|
||||
let path = Path::new(&path_str);
|
||||
if !path.exists() || !path.is_dir() {
|
||||
return Ok(PathStatus::Empty); // Treat non-existent paths as empty for this purpose
|
||||
}
|
||||
|
||||
if path.file_name().and_then(|n| n.to_str()) == Some("pastebar-data") {
|
||||
if path.read_dir().map_err(|e| e.to_string())?.next().is_some() {
|
||||
return Ok(PathStatus::IsPastebarDataAndNotEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
if path.read_dir().map_err(|e| e.to_string())?.next().is_some() {
|
||||
return Ok(PathStatus::NotEmpty);
|
||||
}
|
||||
|
||||
Ok(PathStatus::Empty)
|
||||
}
|
||||
|
||||
/// Returns the current `custom_db_path` (if any).
|
||||
#[command]
|
||||
@ -13,16 +49,183 @@ pub fn cmd_get_custom_db_path() -> Option<String> {
|
||||
get_custom_db_path()
|
||||
}
|
||||
|
||||
/// Insert or update a new `custom_db_path`.
|
||||
// cmd_set_custom_db_path is now part of cmd_set_and_relocate_db
|
||||
// cmd_remove_custom_db_path is now part of cmd_revert_to_default_db_location
|
||||
|
||||
/// Validates if the provided path is a writable directory.
|
||||
#[command]
|
||||
pub fn cmd_set_custom_db_path(new_path: String) -> Result<(), String> {
|
||||
set_custom_db_path(&new_path)
|
||||
pub fn cmd_validate_custom_db_path(path_str: String) -> Result<bool, String> {
|
||||
let path = Path::new(&path_str);
|
||||
if !path.exists() {
|
||||
// Attempt to create it if it doesn't exist, to check writability of parent
|
||||
if let Some(parent) = path.parent() {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent).map_err(|e| {
|
||||
format!(
|
||||
"Failed to create parent directory {}: {}",
|
||||
parent.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
// Check if we can create the directory itself (simulates future db file creation in this dir)
|
||||
fs::create_dir_all(&path).map_err(|e| {
|
||||
format!(
|
||||
"Path {} is not a valid directory or cannot be created: {}",
|
||||
path.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
// Clean up by removing the directory if we created it for validation
|
||||
fs::remove_dir(&path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to clean up validation directory {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
} else if !path.is_dir() {
|
||||
return Err(format!("Path {} is not a directory.", path_str));
|
||||
}
|
||||
|
||||
// Check writability by trying to create a temporary file
|
||||
let temp_file_path = path.join(".tmp_pastebar_writable_check");
|
||||
match fs::File::create(&temp_file_path) {
|
||||
Ok(_) => {
|
||||
fs::remove_file(&temp_file_path)
|
||||
.map_err(|e| format!("Failed to remove temporary check file: {}", e))?;
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => Err(format!("Directory {} is not writable: {}", path_str, e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove (clear) the `custom_db_path`.
|
||||
/// Sets the custom data path, and moves/copies the data directory.
|
||||
#[command]
|
||||
pub fn cmd_remove_custom_db_path() -> Result<(), String> {
|
||||
remove_custom_db_path()
|
||||
pub fn cmd_set_and_relocate_data(
|
||||
new_parent_dir_path: String,
|
||||
operation: String,
|
||||
) -> Result<String, String> {
|
||||
let current_data_dir = get_data_dir();
|
||||
let new_data_dir = PathBuf::from(&new_parent_dir_path);
|
||||
|
||||
fs::create_dir_all(&new_data_dir)
|
||||
.map_err(|e| format!("Failed to create new data directory: {}", e))?;
|
||||
|
||||
let items_to_relocate = vec!["pastebar-db.data", "clip-images", "clipboard-images"];
|
||||
|
||||
for item_name in items_to_relocate {
|
||||
let source_path = current_data_dir.join(item_name);
|
||||
let dest_path = new_data_dir.join(item_name);
|
||||
|
||||
if !source_path.exists() {
|
||||
println!(
|
||||
"Source item {} does not exist, skipping.",
|
||||
source_path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
match operation.as_str() {
|
||||
"move" => {
|
||||
if source_path.is_dir() {
|
||||
fs::rename(&source_path, &dest_path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to move directory {} to {}: {}",
|
||||
source_path.display(),
|
||||
dest_path.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
fs::rename(&source_path, &dest_path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to move file {} to {}: {}",
|
||||
source_path.display(),
|
||||
dest_path.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
"copy" => {
|
||||
if source_path.is_dir() {
|
||||
let mut options = CopyOptions::new();
|
||||
options.overwrite = true;
|
||||
copy(&source_path, &dest_path, &options)
|
||||
.map_err(|e| format!("Failed to copy directory: {}", e))?;
|
||||
} else {
|
||||
fs::copy(&source_path, &dest_path).map_err(|e| format!("Failed to copy file: {}", e))?;
|
||||
}
|
||||
}
|
||||
"none" => {
|
||||
// Do nothing, just switch to the new location
|
||||
}
|
||||
_ => return Err("Invalid operation specified. Use 'move', 'copy', or 'none'.".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
user_settings_service::set_custom_db_path(&new_parent_dir_path)?;
|
||||
|
||||
Ok(format!(
|
||||
"Data successfully {} to {}. Please restart the application.",
|
||||
operation,
|
||||
new_data_dir.display()
|
||||
))
|
||||
}
|
||||
|
||||
/// Clears the custom data path and optionally moves the data back to default.
|
||||
#[command]
|
||||
pub fn cmd_revert_to_default_data_location(
|
||||
move_files_back: bool,
|
||||
overwrite_default: bool,
|
||||
) -> Result<String, String> {
|
||||
let current_custom_data_dir = get_data_dir();
|
||||
let default_data_dir = get_default_data_dir();
|
||||
|
||||
if move_files_back && current_custom_data_dir != default_data_dir {
|
||||
let items_to_relocate = vec!["pastebar-db.data", "clip-images", "clipboard-images"];
|
||||
|
||||
for item_name in items_to_relocate {
|
||||
let source_path = current_custom_data_dir.join(item_name);
|
||||
let dest_path = default_data_dir.join(item_name);
|
||||
|
||||
if !source_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if dest_path.exists() && !overwrite_default {
|
||||
return Err(format!(
|
||||
"Default item at {} already exists. Cannot overwrite without explicit confirmation.",
|
||||
dest_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
if source_path.is_dir() {
|
||||
fs::rename(&source_path, &dest_path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to move directory {} to {}: {}",
|
||||
source_path.display(),
|
||||
dest_path.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
fs::rename(&source_path, &dest_path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to move file {} to {}: {}",
|
||||
source_path.display(),
|
||||
dest_path.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_settings_service::remove_custom_db_path()?;
|
||||
Ok("Data location reverted to default. Please restart the application.".to_string())
|
||||
}
|
||||
|
||||
/// Return all key-value pairs from the `data` map.
|
||||
|
@ -217,59 +217,47 @@ fn db_file_exists() -> bool {
|
||||
Path::new(&db_path).exists()
|
||||
}
|
||||
|
||||
fn get_db_path() -> String {
|
||||
/// Returns the base directory for application data.
|
||||
/// This will be a `pastebar-data` subdirectory if a custom path is set.
|
||||
pub fn get_data_dir() -> PathBuf {
|
||||
let user_config = load_user_config();
|
||||
|
||||
if let Some(custom_path) = user_config.custom_db_path {
|
||||
let final_path = adjust_custom_db_path(&custom_path);
|
||||
|
||||
// Check if it's valid/writable
|
||||
if can_access_or_create(&final_path) {
|
||||
return final_path;
|
||||
} else {
|
||||
eprintln!(
|
||||
"Warning: custom_db_path=\"{}\" is invalid or not writable. Falling back to default...",
|
||||
custom_path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
let app_dir = APP_CONSTANTS.get().unwrap().app_dev_data_dir.clone();
|
||||
let path = if cfg!(target_os = "macos") {
|
||||
format!(
|
||||
"{}/local.pastebar-db.data",
|
||||
adjust_canonicalization(app_dir)
|
||||
)
|
||||
} else if cfg!(target_os = "windows") {
|
||||
format!(
|
||||
"{}\\local.pastebar-db.data",
|
||||
adjust_canonicalization(app_dir)
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{}/local.pastebar-db.data",
|
||||
adjust_canonicalization(app_dir)
|
||||
)
|
||||
};
|
||||
|
||||
path
|
||||
if let Some(custom_path_str) = user_config.custom_db_path {
|
||||
PathBuf::from(custom_path_str)
|
||||
} else {
|
||||
let app_data_dir = APP_CONSTANTS.get().unwrap().app_data_dir.clone();
|
||||
let data_dir = app_data_dir.as_path();
|
||||
|
||||
let path = if cfg!(target_os = "macos") {
|
||||
format!("{}/pastebar-db.data", adjust_canonicalization(data_dir))
|
||||
} else if cfg!(target_os = "windows") {
|
||||
format!("{}\\pastebar-db.data", adjust_canonicalization(data_dir))
|
||||
} else {
|
||||
format!("{}/pastebar-db.data", adjust_canonicalization(data_dir))
|
||||
};
|
||||
|
||||
path
|
||||
get_default_data_dir()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default application data directory.
|
||||
pub fn get_default_data_dir() -> PathBuf {
|
||||
if cfg!(debug_assertions) {
|
||||
APP_CONSTANTS.get().unwrap().app_dev_data_dir.clone()
|
||||
} else {
|
||||
APP_CONSTANTS.get().unwrap().app_data_dir.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_db_path() -> String {
|
||||
let db_path = get_data_dir().join("pastebar-db.data");
|
||||
db_path.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
/// Returns the path to the `clip-images` directory.
|
||||
pub fn get_clip_images_dir() -> PathBuf {
|
||||
get_data_dir().join("clip-images")
|
||||
}
|
||||
|
||||
/// Returns the path to the `clipboard-images` directory.
|
||||
pub fn get_clipboard_images_dir() -> PathBuf {
|
||||
get_data_dir().join("clipboard-images")
|
||||
}
|
||||
|
||||
/// Returns the default database file path as a string.
|
||||
pub fn get_default_db_path_string() -> String {
|
||||
let db_path = get_default_data_dir().join("pastebar-db.data");
|
||||
db_path.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
fn can_access_or_create(db_path: &str) -> bool {
|
||||
let path = std::path::Path::new(db_path);
|
||||
|
||||
@ -298,33 +286,13 @@ fn can_access_or_create(db_path: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn adjust_custom_db_path(custom_path: &str) -> String {
|
||||
use std::path::PathBuf;
|
||||
let path = PathBuf::from(custom_path);
|
||||
|
||||
match fs::metadata(&path) {
|
||||
Ok(metadata) => {
|
||||
if metadata.is_dir() {
|
||||
// It's a directory, so append "pastebar-db.data" to it
|
||||
let mut dir_path = path.clone();
|
||||
dir_path.push("pastebar-db.data");
|
||||
dir_path.to_string_lossy().into_owned()
|
||||
} else {
|
||||
// It's a file or symlink, so leave it as is
|
||||
custom_path.to_string()
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// If we can’t read metadata (e.g. it doesn't exist yet),
|
||||
// we treat `custom_path` as a file path already.
|
||||
custom_path.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_config_file_path() -> PathBuf {
|
||||
if cfg!(debug_assertions) {
|
||||
let app_dir = APP_CONSTANTS.get().unwrap().app_dev_data_dir.clone();
|
||||
let app_dir = APP_CONSTANTS
|
||||
.get()
|
||||
.expect("APP_CONSTANTS not initialized")
|
||||
.app_dev_data_dir
|
||||
.clone();
|
||||
if cfg!(target_os = "macos") {
|
||||
PathBuf::from(format!(
|
||||
"{}/pastebar_settings.yaml",
|
||||
|
@ -1185,8 +1185,12 @@ async fn main() {
|
||||
security_commands::delete_os_password,
|
||||
security_commands::get_stored_os_password,
|
||||
user_settings_command::cmd_get_custom_db_path,
|
||||
user_settings_command::cmd_set_custom_db_path,
|
||||
user_settings_command::cmd_remove_custom_db_path,
|
||||
// user_settings_command::cmd_set_custom_db_path, // Replaced by cmd_set_and_relocate_db
|
||||
// user_settings_command::cmd_remove_custom_db_path, // Replaced by cmd_revert_to_default_db_location
|
||||
user_settings_command::cmd_validate_custom_db_path,
|
||||
user_settings_command::cmd_check_custom_data_path,
|
||||
user_settings_command::cmd_set_and_relocate_data,
|
||||
user_settings_command::cmd_revert_to_default_data_location,
|
||||
user_settings_command::cmd_get_all_settings,
|
||||
user_settings_command::cmd_get_setting,
|
||||
user_settings_command::cmd_set_setting,
|
||||
|
@ -32,7 +32,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::db::APP_CONSTANTS;
|
||||
use crate::db::{self, APP_CONSTANTS};
|
||||
use crate::schema::clipboard_history;
|
||||
use crate::schema::clipboard_history::dsl::*;
|
||||
use crate::schema::link_metadata;
|
||||
@ -226,11 +226,7 @@ pub fn add_clipboard_history_from_image(
|
||||
let _history_id = nanoid!().to_string();
|
||||
let folder_name = &_history_id[..3];
|
||||
|
||||
let base_dir = if cfg!(debug_assertions) {
|
||||
&APP_CONSTANTS.get().unwrap().app_dev_data_dir
|
||||
} else {
|
||||
&APP_CONSTANTS.get().unwrap().app_data_dir
|
||||
};
|
||||
let base_dir = db::get_clipboard_images_dir();
|
||||
|
||||
let (_image_width, _image_height) = image.dimensions();
|
||||
|
||||
@ -279,7 +275,7 @@ pub fn add_clipboard_history_from_image(
|
||||
))
|
||||
.execute(connection);
|
||||
} else {
|
||||
let folder_path = base_dir.join("clipboard-images").join(folder_name);
|
||||
let folder_path = base_dir.join(folder_name);
|
||||
ensure_dir_exists(&folder_path);
|
||||
|
||||
let image_file_name = folder_path.join(format!("{}.png", &_history_id));
|
||||
@ -712,13 +708,7 @@ pub fn delete_recent_clipboard_history(
|
||||
pub fn delete_all_clipboard_histories() -> String {
|
||||
let connection = &mut establish_pool_db_connection();
|
||||
|
||||
let base_dir = if cfg!(debug_assertions) {
|
||||
&APP_CONSTANTS.get().unwrap().app_dev_data_dir
|
||||
} else {
|
||||
&APP_CONSTANTS.get().unwrap().app_data_dir
|
||||
};
|
||||
|
||||
let folder_path = base_dir.join("clipboard-images");
|
||||
let folder_path = db::get_clipboard_images_dir();
|
||||
|
||||
let _ = remove_dir_if_exists(&folder_path);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::db::APP_CONSTANTS;
|
||||
use crate::db::{self, APP_CONSTANTS};
|
||||
use crate::models::models::UpdatedItemData;
|
||||
use crate::models::Item;
|
||||
use crate::services::utils::debug_output;
|
||||
@ -509,13 +509,7 @@ pub fn add_image_to_item(item_id: &str, image_full_path: &str) -> Result<String,
|
||||
|
||||
let is_svg = extension == "svg";
|
||||
|
||||
let base_dir = if cfg!(debug_assertions) {
|
||||
&APP_CONSTANTS.get().unwrap().app_dev_data_dir
|
||||
} else {
|
||||
&APP_CONSTANTS.get().unwrap().app_data_dir
|
||||
};
|
||||
|
||||
let folder_path = base_dir.join("clip-images").join(&item_id[..3]);
|
||||
let folder_path = db::get_clip_images_dir().join(&item_id[..3]);
|
||||
ensure_dir_exists(&folder_path);
|
||||
let new_image_path = folder_path.join(format!("{}.{}", item_id, extension));
|
||||
|
||||
@ -636,13 +630,7 @@ pub fn save_item_image_from_history_item(
|
||||
) -> Result<String, String> {
|
||||
let folder_name = &item_id[..3];
|
||||
|
||||
let base_dir = if cfg!(debug_assertions) {
|
||||
&APP_CONSTANTS.get().unwrap().app_dev_data_dir
|
||||
} else {
|
||||
&APP_CONSTANTS.get().unwrap().app_data_dir
|
||||
};
|
||||
|
||||
let folder_path = base_dir.join("clip-images").join(folder_name);
|
||||
let folder_path = db::get_clip_images_dir().join(folder_name);
|
||||
ensure_dir_exists(&folder_path);
|
||||
|
||||
let clip_image_file_name = folder_path.join(format!("{}.png", &item_id));
|
||||
@ -684,13 +672,7 @@ pub fn upload_image_file_to_item_id(
|
||||
|
||||
let file_name = format!("{}.{}", item_id, extension);
|
||||
|
||||
let base_dir = if cfg!(debug_assertions) {
|
||||
&APP_CONSTANTS.get().unwrap().app_dev_data_dir
|
||||
} else {
|
||||
&APP_CONSTANTS.get().unwrap().app_data_dir
|
||||
};
|
||||
|
||||
let folder_path = base_dir.join("clip-images").join(&item_id[..3]);
|
||||
let folder_path = db::get_clip_images_dir().join(&item_id[..3]);
|
||||
ensure_dir_exists(&folder_path);
|
||||
let image_path = folder_path.join(&file_name);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user