Initial Comicviewer implementation
This commit is contained in:
commit
cb01395612
20 changed files with 4940 additions and 0 deletions
152
src/config.rs
Normal file
152
src/config.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct AppConfig {
|
||||
pub cache_quota_gb: u64,
|
||||
pub fast_image_loading: bool,
|
||||
pub locations: Vec<LocationProfile>,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cache_quota_gb: 10,
|
||||
fast_image_loading: true,
|
||||
locations: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LocationProfile {
|
||||
pub name: String,
|
||||
pub kind: LocationKind,
|
||||
pub address: String,
|
||||
#[serde(default)]
|
||||
pub username: String,
|
||||
#[serde(default)]
|
||||
pub password: String,
|
||||
#[serde(default)]
|
||||
pub domain: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LocationKind {
|
||||
Local,
|
||||
Smb,
|
||||
Ftp,
|
||||
Sftp,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn load_or_create(path: &Path) -> Result<Self> {
|
||||
if path.exists() {
|
||||
restrict_file_permissions(path)?;
|
||||
let contents = fs::read_to_string(path)
|
||||
.with_context(|| format!("설정 파일을 읽을 수 없습니다: {}", path.display()))?;
|
||||
let config = toml::from_str(&contents)
|
||||
.with_context(|| format!("설정 파일 형식이 잘못되었습니다: {}", path.display()))?;
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
let config = Self::default();
|
||||
config.save(path)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn save(&self, path: &Path) -> Result<()> {
|
||||
let temporary_path = temporary_path(path);
|
||||
let contents = toml::to_string_pretty(self).context("설정을 직렬화할 수 없습니다")?;
|
||||
|
||||
let write_result = write_private_file(&temporary_path, contents.as_bytes())
|
||||
.and_then(|()| fs::rename(&temporary_path, path).map_err(Into::into));
|
||||
|
||||
if write_result.is_err() {
|
||||
let _ = fs::remove_file(&temporary_path);
|
||||
}
|
||||
write_result
|
||||
.with_context(|| format!("설정 파일을 저장할 수 없습니다: {}", path.display()))?;
|
||||
restrict_file_permissions(path)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn temporary_path(path: &Path) -> PathBuf {
|
||||
let mut file_name = path
|
||||
.file_name()
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| "config".into());
|
||||
file_name.push(".tmp");
|
||||
path.with_file_name(file_name)
|
||||
}
|
||||
|
||||
fn write_private_file(path: &Path, contents: &[u8]) -> Result<()> {
|
||||
let mut options = OpenOptions::new();
|
||||
options.write(true).create(true).truncate(true);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
options.mode(0o600);
|
||||
}
|
||||
|
||||
let mut file = options.open(path)?;
|
||||
file.write_all(contents)?;
|
||||
file.sync_all()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restrict_file_permissions(path: &Path) -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let mut permissions = fs::metadata(path)?.permissions();
|
||||
permissions.set_mode(0o600);
|
||||
fs::set_permissions(path, permissions)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_cache_quota_is_ten_gigabytes() {
|
||||
assert_eq!(AppConfig::default().cache_quota_gb, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn location_credentials_round_trip_as_plain_text() {
|
||||
let config = AppConfig {
|
||||
cache_quota_gb: 0,
|
||||
fast_image_loading: true,
|
||||
locations: vec![LocationProfile {
|
||||
name: "테스트".into(),
|
||||
kind: LocationKind::Sftp,
|
||||
address: "sftp://example.invalid/comics".into(),
|
||||
username: "reader".into(),
|
||||
password: "plain-password".into(),
|
||||
domain: String::new(),
|
||||
}],
|
||||
};
|
||||
|
||||
let encoded = toml::to_string(&config).unwrap();
|
||||
let decoded: AppConfig = toml::from_str(&encoded).unwrap();
|
||||
assert_eq!(decoded.locations[0].password, "plain-password");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_image_loading_is_enabled_for_existing_configs() {
|
||||
let decoded: AppConfig = toml::from_str("cache_quota_gb = 10\n").unwrap();
|
||||
assert!(decoded.fast_image_loading);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue