feat: atomic writing (#1673)

* feat: atomic writing

* Apply suggestions from code review

* clean up temp files on error

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
J M
2022-07-22 14:08:49 -06:00
committed by GitHub
parent 15af827cbc
commit c22c80d3b0
12 changed files with 357 additions and 122 deletions

7
backend/Cargo.lock generated
View File

@@ -558,9 +558,9 @@ dependencies = [
[[package]] [[package]]
name = "color-eyre" name = "color-eyre"
version = "0.6.1" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ebf286c900a6d5867aeff75cfee3192857bb7f24b547d4f0df2ed6baa812c90" checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"color-spantrace", "color-spantrace",
@@ -1619,6 +1619,8 @@ dependencies = [
name = "helpers" name = "helpers"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"color-eyre",
"futures",
"models", "models",
"pin-project", "pin-project",
"tokio", "tokio",
@@ -1940,6 +1942,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.10.2",
"swc_atoms", "swc_atoms",
"swc_common", "swc_common",
"swc_config", "swc_config",

View File

@@ -1,9 +1,11 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use chrono::Utc; use chrono::Utc;
use clap::ArgMatches; use clap::ArgMatches;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use helpers::AtomicFile;
use openssl::pkey::{PKey, Private}; use openssl::pkey::{PKey, Private};
use openssl::x509::X509; use openssl::x509::X509;
use patch_db::{DbHandle, LockType, PatchDbHandle, Revision}; use patch_db::{DbHandle, LockType, PatchDbHandle, Revision};
@@ -27,10 +29,10 @@ use crate::disk::mount::guard::TmpMountGuard;
use crate::notifications::NotificationLevel; use crate::notifications::NotificationLevel;
use crate::s9pk::manifest::PackageId; use crate::s9pk::manifest::PackageId;
use crate::status::MainStatus; use crate::status::MainStatus;
use crate::util::display_none;
use crate::util::serde::IoFormat; use crate::util::serde::IoFormat;
use crate::util::{display_none, AtomicFile};
use crate::version::VersionT; use crate::version::VersionT;
use crate::Error; use crate::{Error, ErrorKind, ResultExt};
#[derive(Debug)] #[derive(Debug)]
pub struct OsBackup { pub struct OsBackup {
@@ -424,7 +426,12 @@ async fn perform_backup<Db: DbHandle>(
.await?; .await?;
let (root_ca_key, root_ca_cert) = ctx.net_controller.ssl.export_root_ca().await?; let (root_ca_key, root_ca_cert) = ctx.net_controller.ssl.export_root_ca().await?;
let mut os_backup_file = AtomicFile::new(backup_guard.as_ref().join("os-backup.cbor")).await?; let mut os_backup_file = AtomicFile::new(
backup_guard.as_ref().join("os-backup.cbor"),
None::<PathBuf>,
)
.await
.with_kind(ErrorKind::Filesystem)?;
os_backup_file os_backup_file
.write_all( .write_all(
&IoFormat::Cbor.to_vec(&OsBackup { &IoFormat::Cbor.to_vec(&OsBackup {
@@ -439,7 +446,10 @@ async fn perform_backup<Db: DbHandle>(
})?, })?,
) )
.await?; .await?;
os_backup_file.save().await?; os_backup_file
.save()
.await
.with_kind(ErrorKind::Filesystem)?;
let timestamp = Some(Utc::now()); let timestamp = Some(Utc::now());

View File

@@ -1,8 +1,9 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::path::Path; use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use helpers::AtomicFile;
use patch_db::{DbHandle, HasModel, LockType}; use patch_db::{DbHandle, HasModel, LockType};
use rpc_toolkit::command; use rpc_toolkit::command;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -20,10 +21,10 @@ use crate::net::interface::{InterfaceId, Interfaces};
use crate::procedure::{NoOutput, PackageProcedure, ProcedureName}; use crate::procedure::{NoOutput, PackageProcedure, ProcedureName};
use crate::s9pk::manifest::PackageId; use crate::s9pk::manifest::PackageId;
use crate::util::serde::IoFormat; use crate::util::serde::IoFormat;
use crate::util::{AtomicFile, Version}; use crate::util::Version;
use crate::version::{Current, VersionT}; use crate::version::{Current, VersionT};
use crate::volume::{backup_dir, Volume, VolumeId, Volumes, BACKUP_DIR}; use crate::volume::{backup_dir, Volume, VolumeId, Volumes, BACKUP_DIR};
use crate::{Error, ResultExt}; use crate::{Error, ErrorKind, ResultExt};
pub mod backup_bulk; pub mod backup_bulk;
pub mod restore; pub mod restore;
@@ -129,7 +130,9 @@ impl BackupActions {
.join(pkg_version.as_str()) .join(pkg_version.as_str())
.join(format!("{}.s9pk", pkg_id)); .join(format!("{}.s9pk", pkg_id));
let mut infile = File::open(&s9pk_path).await?; let mut infile = File::open(&s9pk_path).await?;
let mut outfile = AtomicFile::new(&tmp_path).await?; let mut outfile = AtomicFile::new(&tmp_path, None::<PathBuf>)
.await
.with_kind(ErrorKind::Filesystem)?;
tokio::io::copy(&mut infile, &mut *outfile) tokio::io::copy(&mut infile, &mut *outfile)
.await .await
.with_ctx(|_| { .with_ctx(|_| {
@@ -138,17 +141,19 @@ impl BackupActions {
format!("cp {} -> {}", s9pk_path.display(), tmp_path.display()), format!("cp {} -> {}", s9pk_path.display(), tmp_path.display()),
) )
})?; })?;
outfile.save().await?; outfile.save().await.with_kind(ErrorKind::Filesystem)?;
let timestamp = Utc::now(); let timestamp = Utc::now();
let metadata_path = Path::new(BACKUP_DIR).join(pkg_id).join("metadata.cbor"); let metadata_path = Path::new(BACKUP_DIR).join(pkg_id).join("metadata.cbor");
let mut outfile = AtomicFile::new(&metadata_path).await?; let mut outfile = AtomicFile::new(&metadata_path, None::<PathBuf>)
.await
.with_kind(ErrorKind::Filesystem)?;
outfile outfile
.write_all(&IoFormat::Cbor.to_vec(&BackupMetadata { .write_all(&IoFormat::Cbor.to_vec(&BackupMetadata {
timestamp, timestamp,
tor_keys, tor_keys,
})?) })?)
.await?; .await?;
outfile.save().await?; outfile.save().await.with_kind(ErrorKind::Filesystem)?;
Ok(PackageBackupInfo { Ok(PackageBackupInfo {
os_version: Current::new().semver().into(), os_version: Current::new().semver().into(),
title: pkg_title.to_owned(), title: pkg_title.to_owned(),

View File

@@ -7,6 +7,7 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use bollard::Docker; use bollard::Docker;
use helpers::to_tmp_path;
use patch_db::json_ptr::JsonPointer; use patch_db::json_ptr::JsonPointer;
use patch_db::{DbHandle, LockReceipt, LockType, PatchDb, Revision}; use patch_db::{DbHandle, LockReceipt, LockType, PatchDb, Revision};
use reqwest::Url; use reqwest::Url;
@@ -35,7 +36,7 @@ use crate::shutdown::Shutdown;
use crate::status::{MainStatus, Status}; use crate::status::{MainStatus, Status};
use crate::util::io::from_yaml_async_reader; use crate::util::io::from_yaml_async_reader;
use crate::util::{AsyncFileExt, Invoke}; use crate::util::{AsyncFileExt, Invoke};
use crate::{Error, ResultExt}; use crate::{volume, Error, ErrorKind, ResultExt};
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
@@ -336,6 +337,18 @@ impl RpcContext {
static_files, static_files,
manifest, manifest,
} => { } => {
for (volume_id, volume_info) in &*manifest.volumes {
let tmp_path = to_tmp_path(volume_info.path_for(
&self.datadir,
&package_id,
&manifest.version,
&volume_id,
))
.with_kind(ErrorKind::Filesystem)?;
if tokio::fs::metadata(&tmp_path).await.is_ok() {
tokio::fs::remove_dir_all(&tmp_path).await?;
}
}
let status = installed.status; let status = installed.status;
let main = match status.main { let main = match status.main {
MainStatus::BackingUp { started, .. } => { MainStatus::BackingUp { started, .. } => {

View File

@@ -1,6 +1,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use helpers::AtomicFile;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tracing::instrument; use tracing::instrument;
@@ -14,9 +15,9 @@ use crate::disk::util::EmbassyOsRecoveryInfo;
use crate::middleware::encrypt::{decrypt_slice, encrypt_slice}; use crate::middleware::encrypt::{decrypt_slice, encrypt_slice};
use crate::s9pk::manifest::PackageId; use crate::s9pk::manifest::PackageId;
use crate::util::serde::IoFormat; use crate::util::serde::IoFormat;
use crate::util::{AtomicFile, FileLock}; use crate::util::FileLock;
use crate::volume::BACKUP_DIR; use crate::volume::BACKUP_DIR;
use crate::{Error, ResultExt}; use crate::{Error, ErrorKind, ResultExt};
pub struct BackupMountGuard<G: GenericMountGuard> { pub struct BackupMountGuard<G: GenericMountGuard> {
backup_disk_mount_guard: Option<G>, backup_disk_mount_guard: Option<G>,
@@ -162,16 +163,20 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
pub async fn save(&self) -> Result<(), Error> { pub async fn save(&self) -> Result<(), Error> {
let metadata_path = self.as_ref().join("metadata.cbor"); let metadata_path = self.as_ref().join("metadata.cbor");
let backup_disk_path = self.backup_disk_path(); let backup_disk_path = self.backup_disk_path();
let mut file = AtomicFile::new(&metadata_path).await?; let mut file = AtomicFile::new(&metadata_path, None::<PathBuf>)
.await
.with_kind(ErrorKind::Filesystem)?;
file.write_all(&IoFormat::Cbor.to_vec(&self.metadata)?) file.write_all(&IoFormat::Cbor.to_vec(&self.metadata)?)
.await?; .await?;
file.save().await?; file.save().await.with_kind(ErrorKind::Filesystem)?;
let unencrypted_metadata_path = let unencrypted_metadata_path =
backup_disk_path.join("EmbassyBackups/unencrypted-metadata.cbor"); backup_disk_path.join("EmbassyBackups/unencrypted-metadata.cbor");
let mut file = AtomicFile::new(&unencrypted_metadata_path).await?; let mut file = AtomicFile::new(&unencrypted_metadata_path, None::<PathBuf>)
.await
.with_kind(ErrorKind::Filesystem)?;
file.write_all(&IoFormat::Cbor.to_vec(&self.unencrypted_metadata)?) file.write_all(&IoFormat::Cbor.to_vec(&self.unencrypted_metadata)?)
.await?; .await?;
file.save().await?; file.save().await.with_kind(ErrorKind::Filesystem)?;
Ok(()) Ok(())
} }

View File

@@ -1,14 +1,14 @@
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::num::ParseIntError; use std::num::ParseIntError;
use std::path::Path; use std::path::{Path, PathBuf};
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use helpers::AtomicFile;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tracing::instrument; use tracing::instrument;
use super::BOOT_RW_PATH; use super::BOOT_RW_PATH;
use crate::util::AtomicFile; use crate::{Error, ErrorKind, ResultExt};
use crate::Error;
pub const QUIRK_PATH: &'static str = "/sys/module/usb_storage/parameters/quirks"; pub const QUIRK_PATH: &'static str = "/sys/module/usb_storage/parameters/quirks";
@@ -160,11 +160,13 @@ pub async fn save_quirks(quirks: &Quirks) -> Result<(), Error> {
tokio::fs::copy(&target_path, &orig_path).await?; tokio::fs::copy(&target_path, &orig_path).await?;
} }
let cmdline = tokio::fs::read_to_string(&orig_path).await?; let cmdline = tokio::fs::read_to_string(&orig_path).await?;
let mut target = AtomicFile::new(&target_path).await?; let mut target = AtomicFile::new(&target_path, None::<PathBuf>)
.await
.with_kind(ErrorKind::Filesystem)?;
target target
.write_all(format!("usb-storage.quirks={} {}", quirks, cmdline).as_bytes()) .write_all(format!("usb-storage.quirks={} {}", quirks, cmdline).as_bytes())
.await?; .await?;
target.save().await?; target.save().await.with_kind(ErrorKind::Filesystem)?;
Ok(()) Ok(())
} }

View File

@@ -11,8 +11,7 @@ use async_trait::async_trait;
use clap::ArgMatches; use clap::ArgMatches;
use color_eyre::eyre::{self, eyre}; use color_eyre::eyre::{self, eyre};
use fd_lock_rs::FdLock; use fd_lock_rs::FdLock;
use futures::future::BoxFuture; use helpers::canonicalize;
use futures::FutureExt;
pub use helpers::NonDetachingJoinHandle; pub use helpers::NonDetachingJoinHandle;
use lazy_static::lazy_static; use lazy_static::lazy_static;
pub use models::Version; pub use models::Version;
@@ -23,7 +22,7 @@ use tokio::sync::{Mutex, OwnedMutexGuard, RwLock};
use tracing::instrument; use tracing::instrument;
use crate::shutdown::Shutdown; use crate::shutdown::Shutdown;
use crate::{Error, ResultExt as _}; use crate::{Error, ErrorKind, ResultExt as _};
pub mod config; pub mod config;
pub mod io; pub mod io;
pub mod logger; pub mod logger;
@@ -273,40 +272,6 @@ impl<F: FnOnce() -> T, T> Drop for GeneralGuard<F, T> {
} }
} }
pub async fn canonicalize(
path: impl AsRef<Path> + Send + Sync,
create_parent: bool,
) -> Result<PathBuf, Error> {
fn create_canonical_folder<'a>(
path: impl AsRef<Path> + Send + Sync + 'a,
) -> BoxFuture<'a, Result<PathBuf, Error>> {
async move {
let path = canonicalize(path, true).await?;
tokio::fs::create_dir(&path)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, path.display().to_string()))?;
Ok(path)
}
.boxed()
}
let path = path.as_ref();
if tokio::fs::metadata(path).await.is_err() {
if let (Some(parent), Some(file_name)) = (path.parent(), path.file_name()) {
if create_parent && tokio::fs::metadata(parent).await.is_err() {
return Ok(create_canonical_folder(parent).await?.join(file_name));
} else {
return Ok(tokio::fs::canonicalize(parent)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, parent.display().to_string()))?
.join(file_name));
}
}
}
tokio::fs::canonicalize(&path)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, path.display().to_string()))
}
pub struct FileLock(OwnedMutexGuard<()>, Option<FdLock<File>>); pub struct FileLock(OwnedMutexGuard<()>, Option<FdLock<File>>);
impl Drop for FileLock { impl Drop for FileLock {
fn drop(&mut self) { fn drop(&mut self) {
@@ -322,7 +287,9 @@ impl FileLock {
static ref INTERNAL_LOCKS: Mutex<BTreeMap<PathBuf, Arc<Mutex<()>>>> = static ref INTERNAL_LOCKS: Mutex<BTreeMap<PathBuf, Arc<Mutex<()>>>> =
Mutex::new(BTreeMap::new()); Mutex::new(BTreeMap::new());
} }
let path = canonicalize(path.as_ref(), true).await?; let path = canonicalize(path.as_ref(), true)
.await
.with_kind(ErrorKind::Filesystem)?;
let mut internal_locks = INTERNAL_LOCKS.lock().await; let mut internal_locks = INTERNAL_LOCKS.lock().await;
if !internal_locks.contains_key(&path) { if !internal_locks.contains_key(&path) {
internal_locks.insert(path.clone(), Arc::new(Mutex::new(()))); internal_locks.insert(path.clone(), Arc::new(Mutex::new(())));
@@ -362,58 +329,3 @@ impl FileLock {
Ok(()) Ok(())
} }
} }
pub struct AtomicFile {
tmp_path: PathBuf,
path: PathBuf,
file: File,
}
impl AtomicFile {
pub async fn new(path: impl AsRef<Path> + Send + Sync) -> Result<Self, Error> {
let path = canonicalize(&path, true).await?;
let tmp_path = if let (Some(parent), Some(file_name)) =
(path.parent(), path.file_name().and_then(|f| f.to_str()))
{
parent.join(format!(".{}.tmp", file_name))
} else {
return Err(Error::new(
eyre!("invalid path: {}", path.display()),
crate::ErrorKind::Filesystem,
));
};
let file = File::create(&tmp_path)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, tmp_path.display().to_string()))?;
Ok(Self {
tmp_path,
path,
file,
})
}
pub async fn save(mut self) -> Result<(), Error> {
use tokio::io::AsyncWriteExt;
self.file.flush().await?;
self.file.shutdown().await?;
self.file.sync_all().await?;
tokio::fs::rename(&self.tmp_path, &self.path)
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
format!("mv {} -> {}", self.tmp_path.display(), self.path.display()),
)
})
}
}
impl std::ops::Deref for AtomicFile {
type Target = File;
fn deref(&self) -> &Self::Target {
&self.file
}
}
impl std::ops::DerefMut for AtomicFile {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.file
}
}

148
libs/Cargo.lock generated
View File

@@ -12,6 +12,21 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "addr2line"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.7.6" version = "0.7.6"
@@ -75,6 +90,21 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.11.0" version = "0.11.0"
@@ -156,6 +186,33 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "color-eyre"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204"
dependencies = [
"backtrace",
"color-spantrace",
"eyre",
"indenter",
"once_cell",
"owo-colors",
"tracing-error 0.2.0",
]
[[package]]
name = "color-spantrace"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce"
dependencies = [
"once_cell",
"owo-colors",
"tracing-core",
"tracing-error 0.2.0",
]
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@@ -390,6 +447,16 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "eyre"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb"
dependencies = [
"indenter",
"once_cell",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "1.7.0" version = "1.7.0"
@@ -586,6 +653,12 @@ dependencies = [
"wasi 0.10.2+wasi-snapshot-preview1", "wasi 0.10.2+wasi-snapshot-preview1",
] ]
[[package]]
name = "gimli"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.13" version = "0.3.13"
@@ -636,6 +709,8 @@ dependencies = [
name = "helpers" name = "helpers"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"color-eyre",
"futures",
"models", "models",
"pin-project", "pin-project",
"tokio", "tokio",
@@ -758,6 +833,12 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.8.2" version = "1.8.2"
@@ -833,6 +914,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"sha2",
"swc_atoms", "swc_atoms",
"swc_common", "swc_common",
"swc_config", "swc_config",
@@ -1022,6 +1104,15 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "miniz_oxide"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
dependencies = [
"adler",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.4" version = "0.8.4"
@@ -1148,6 +1239,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "object"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.12.0" version = "1.12.0"
@@ -1199,6 +1299,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "owo-colors"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@@ -1241,7 +1347,7 @@ dependencies = [
"thiserror", "thiserror",
"tokio", "tokio",
"tracing", "tracing",
"tracing-error", "tracing-error 0.1.2",
] ]
[[package]] [[package]]
@@ -1525,6 +1631,12 @@ dependencies = [
"winreg", "winreg",
] ]
[[package]]
name = "rustc-demangle"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"
@@ -1696,6 +1808,17 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sha2"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.4" version = "0.1.4"
@@ -2369,7 +2492,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24" checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24"
dependencies = [ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber 0.2.25",
]
[[package]]
name = "tracing-error"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e"
dependencies = [
"tracing",
"tracing-subscriber 0.3.11",
] ]
[[package]] [[package]]
@@ -2383,6 +2516,17 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-subscriber"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596"
dependencies = [
"sharded-slab",
"thread_local",
"tracing-core",
]
[[package]] [[package]]
name = "treediff" name = "treediff"
version = "4.0.2" version = "4.0.2"

View File

@@ -6,6 +6,8 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
color-eyre = "0.6.2"
futures = "0.3.21"
pin-project = "1.0.11" pin-project = "1.0.11"
tokio = { version = "1.19.2", features = ["full"] } tokio = { version = "1.19.2", features = ["full"] }
models = { path = "../models" } models = { path = "../models" }

View File

@@ -1,10 +1,60 @@
use std::future::Future; use std::future::Future;
use std::path::{Path, PathBuf};
use color_eyre::eyre::{eyre, Context, Error};
use futures::future::BoxFuture;
use futures::FutureExt;
use tokio::fs::File;
use tokio::task::{JoinError, JoinHandle}; use tokio::task::{JoinError, JoinHandle};
mod script_dir; mod script_dir;
pub use script_dir::*; pub use script_dir::*;
pub fn to_tmp_path(path: impl AsRef<Path>) -> Result<PathBuf, Error> {
let path = path.as_ref();
if let (Some(parent), Some(file_name)) =
(path.parent(), path.file_name().and_then(|f| f.to_str()))
{
Ok(parent.join(format!(".{}.tmp", file_name)))
} else {
Err(eyre!("invalid path: {}", path.display()))
}
}
pub async fn canonicalize(
path: impl AsRef<Path> + Send + Sync,
create_parent: bool,
) -> Result<PathBuf, Error> {
fn create_canonical_folder<'a>(
path: impl AsRef<Path> + Send + Sync + 'a,
) -> BoxFuture<'a, Result<PathBuf, Error>> {
async move {
let path = canonicalize(path, true).await?;
tokio::fs::create_dir(&path)
.await
.with_context(|| path.display().to_string())?;
Ok(path)
}
.boxed()
}
let path = path.as_ref();
if tokio::fs::metadata(path).await.is_err() {
if let (Some(parent), Some(file_name)) = (path.parent(), path.file_name()) {
if create_parent && tokio::fs::metadata(parent).await.is_err() {
return Ok(create_canonical_folder(parent).await?.join(file_name));
} else {
return Ok(tokio::fs::canonicalize(parent)
.await
.with_context(|| parent.display().to_string())?
.join(file_name));
}
}
}
tokio::fs::canonicalize(&path)
.await
.with_context(|| path.display().to_string())
}
#[pin_project::pin_project(PinnedDrop)] #[pin_project::pin_project(PinnedDrop)]
pub struct NonDetachingJoinHandle<T>(#[pin] JoinHandle<T>); pub struct NonDetachingJoinHandle<T>(#[pin] JoinHandle<T>);
impl<T> From<JoinHandle<T>> for NonDetachingJoinHandle<T> { impl<T> From<JoinHandle<T>> for NonDetachingJoinHandle<T> {
@@ -29,3 +79,74 @@ impl<T> Future for NonDetachingJoinHandle<T> {
this.0.poll(cx) this.0.poll(cx)
} }
} }
pub struct AtomicFile {
tmp_path: PathBuf,
path: PathBuf,
file: Option<File>,
}
impl AtomicFile {
pub async fn new(
path: impl AsRef<Path> + Send + Sync,
tmp_path: Option<impl AsRef<Path> + Send + Sync>,
) -> Result<Self, Error> {
let path = canonicalize(&path, true).await?;
let tmp_path = if let Some(tmp_path) = tmp_path {
canonicalize(&tmp_path, true).await?
} else {
to_tmp_path(&path)?
};
let file = File::create(&tmp_path)
.await
.with_context(|| tmp_path.display().to_string())?;
Ok(Self {
tmp_path,
path,
file: Some(file),
})
}
pub async fn rollback(mut self) -> Result<(), Error> {
drop(self.file.take());
tokio::fs::remove_file(&self.tmp_path)
.await
.with_context(|| format!("rm {}", self.tmp_path.display()))?;
Ok(())
}
pub async fn save(mut self) -> Result<(), Error> {
use tokio::io::AsyncWriteExt;
if let Some(file) = self.file.as_mut() {
file.flush().await?;
file.shutdown().await?;
file.sync_all().await?;
}
drop(self.file.take());
tokio::fs::rename(&self.tmp_path, &self.path)
.await
.with_context(|| {
format!("mv {} -> {}", self.tmp_path.display(), self.path.display())
})?;
Ok(())
}
}
impl std::ops::Deref for AtomicFile {
type Target = File;
fn deref(&self) -> &Self::Target {
self.file.as_ref().unwrap()
}
}
impl std::ops::DerefMut for AtomicFile {
fn deref_mut(&mut self) -> &mut Self::Target {
self.file.as_mut().unwrap()
}
}
impl Drop for AtomicFile {
fn drop(&mut self) {
if let Some(file) = self.file.take() {
drop(file);
let path = std::mem::take(&mut self.tmp_path);
tokio::spawn(async move { tokio::fs::remove_file(path).await.unwrap() });
}
}
}

View File

@@ -33,6 +33,7 @@ swc_eq_ignore_macros = "=0.1.0"
swc_macros_common = "=0.3.5" swc_macros_common = "=0.3.5"
swc_visit = "=0.3.0" swc_visit = "=0.3.0"
swc_visit_macros = "=0.3.1" swc_visit_macros = "=0.3.1"
sha2 = "0.10.2"
models = { path = "../models" } models = { path = "../models" }
helpers = { path = "../helpers" } helpers = { path = "../helpers" }
serde = { version = "1.0", features = ["derive", "rc"] } serde = { version = "1.0", features = ["derive", "rc"] }

View File

@@ -347,8 +347,10 @@ mod fns {
use deno_core::anyhow::{anyhow, bail}; use deno_core::anyhow::{anyhow, bail};
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::*; use deno_core::*;
use helpers::{to_tmp_path, AtomicFile};
use models::VolumeId; use models::VolumeId;
use serde_json::Value; use serde_json::Value;
use tokio::io::AsyncWriteExt;
use super::{AnswerState, JsContext}; use super::{AnswerState, JsContext};
use crate::{system_time_as_unix_ms, MetadataJs}; use crate::{system_time_as_unix_ms, MetadataJs};
@@ -514,7 +516,7 @@ mod fns {
bail!("Volume {} is readonly", volume_id); bail!("Volume {} is readonly", volume_id);
} }
let new_file = volume_path.join(path_in); let new_file = volume_path.join(&path_in);
let parent_new_file = new_file let parent_new_file = new_file
.parent() .parent()
.ok_or_else(|| anyhow!("Expecting that file is not root"))?; .ok_or_else(|| anyhow!("Expecting that file is not root"))?;
@@ -526,7 +528,22 @@ mod fns {
volume_path.to_string_lossy(), volume_path.to_string_lossy(),
); );
} }
tokio::fs::write(new_file, write).await?; let new_volume_tmp = to_tmp_path(&volume_path).map_err(|e| anyhow!("{}", e))?;
let hashed_name = {
use sha2::{Digest, Sha256};
use std::os::unix::ffi::OsStrExt;
let mut hasher = Sha256::new();
hasher.update(path_in.as_os_str().as_bytes());
let result = hasher.finalize();
format!("{:X}", result)
};
let temp_file = new_volume_tmp.join(&hashed_name);
let mut file = AtomicFile::new(&new_file, Some(&temp_file))
.await
.map_err(|e| anyhow!("{}", e))?;
file.write_all(write.as_bytes()).await?;
file.save().await.map_err(|e| anyhow!("{}", e))?;
Ok(()) Ok(())
} }
#[op] #[op]