Skip to content

Commit 7293cbf

Browse files
committed
Download/clone sources to a temporary path first
This fixes an issue where if someone tried to reinstall they would be left without any plugins because they would all be nuked up front prior to trying to download them.
1 parent 11ff287 commit 7293cbf

File tree

5 files changed

+211
-88
lines changed

5 files changed

+211
-88
lines changed

src/editor.rs

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::{
88

99
use anyhow::{anyhow, bail, Context as ResultExt, Result};
1010

11-
use crate::edit;
11+
use crate::{edit, util::TempPath};
1212

1313
/// Possible environment variables.
1414
const ENV_VARS: &[&str] = &["VISUAL", "EDITOR"];
@@ -21,12 +21,6 @@ const EDITORS: &[&str] = &["code --wait", "nano", "vim", "vi", "emacs"];
2121
#[cfg(target_os = "windows")]
2222
const EDITORS: &[&str] = &["code.exe --wait", "notepad.exe"];
2323

24-
/// Holds a temporary file path that is removed when dropped.
25-
struct TempFile {
26-
/// The file path that we will edit.
27-
path: PathBuf,
28-
}
29-
3024
/// Represents the default editor.
3125
pub struct Editor {
3226
/// The path to the editor binary.
@@ -40,7 +34,7 @@ pub struct Child {
4034
/// A handle for the editor child process.
4135
child: process::Child,
4236
/// The temporary file that the editor is editing.
43-
file: TempFile,
37+
file: TempPath,
4438
}
4539

4640
/// Convert a string command to a binary and the rest of the arguments.
@@ -54,14 +48,6 @@ where
5448
Some((bin, args))
5549
}
5650

57-
impl TempFile {
58-
/// Create a new `TempFile`.
59-
fn new(original_path: &Path) -> Self {
60-
let path = original_path.with_extension(format!("tmp-{}.toml", process::id()));
61-
Self { path }
62-
}
63-
}
64-
6551
impl Editor {
6652
/// Create a new default `Editor`.
6753
///
@@ -80,12 +66,12 @@ impl Editor {
8066

8167
/// Open a file for editing with initial contents.
8268
pub fn edit(self, path: &Path, contents: &str) -> Result<Child> {
83-
let file = TempFile::new(path);
69+
let file = TempPath::new(path);
8470
let Self { bin, args } = self;
85-
fs::write(&file.path, &contents).context("failed to write to temporary file")?;
71+
fs::write(file.path(), &contents).context("failed to write to temporary file")?;
8672
let child = Command::new(bin)
8773
.args(args)
88-
.arg(&file.path)
74+
.arg(file.path())
8975
.spawn()
9076
.context("failed to spawn editor subprocess")?;
9177
Ok(Child { child, file })
@@ -99,7 +85,7 @@ impl Child {
9985
let exit_status = child.wait()?;
10086
if exit_status.success() {
10187
let contents =
102-
fs::read_to_string(&file.path).context("failed to read from temporary file")?;
88+
fs::read_to_string(file.path()).context("failed to read from temporary file")?;
10389
if contents == original_contents {
10490
bail!("aborted editing!");
10591
} else {
@@ -111,9 +97,3 @@ impl Child {
11197
}
11298
}
11399
}
114-
115-
impl Drop for TempFile {
116-
fn drop(&mut self) {
117-
fs::remove_file(&self.path).ok();
118-
}
119-
}

src/lock.rs

Lines changed: 44 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::{
77
cmp,
88
collections::{HashMap, HashSet},
99
convert::TryInto,
10-
fmt, fs, io,
10+
fmt, fs,
1111
path::{Path, PathBuf},
1212
result, sync,
1313
};
@@ -24,7 +24,7 @@ use walkdir::WalkDir;
2424
use crate::{
2525
config::{Config, ExternalPlugin, GitReference, InlinePlugin, Plugin, Source, Template},
2626
context::{LockContext as Context, Settings, SettingsExt},
27-
util::git,
27+
util::{git, TempPath},
2828
};
2929

3030
/// The maximmum number of threads to use while downloading sources.
@@ -166,55 +166,60 @@ impl Source {
166166
url: Url,
167167
reference: Option<GitReference>,
168168
) -> Result<LockedSource> {
169-
if ctx.reinstall {
170-
if let Err(e) = fs::remove_dir_all(&dir) {
171-
if e.kind() != io::ErrorKind::NotFound {
172-
return Err(e).with_context(s!("failed to remove dir `{}`", &dir.display()));
169+
let checkout = |repo, status| -> Result<()> {
170+
match reference {
171+
Some(reference) => {
172+
git::checkout(&repo, reference.lock(&repo)?.0)?;
173+
git::submodule_update(&repo).context("failed to recursively update")?;
174+
status!(ctx, status, &format!("{}@{}", &url, reference));
175+
}
176+
None => {
177+
git::submodule_update(&repo).context("failed to recursively update")?;
178+
status!(ctx, status, &url);
173179
}
174180
}
175-
}
176-
177-
let (cloned, repo) = git::clone_or_open(&url, &dir)?;
178-
let status = if cloned { "Cloned" } else { "Checked" };
181+
Ok(())
182+
};
179183

180-
// Checkout the configured revision.
181-
if let Some(reference) = reference {
182-
git::checkout(&repo, reference.lock(&repo)?.0)?;
183-
status!(ctx, status, &format!("{}@{}", &url, reference));
184-
} else {
185-
status!(ctx, status, &url);
184+
if !ctx.reinstall {
185+
if let Ok(repo) = git::open(&dir) {
186+
checkout(repo, "Checked")?;
187+
return Ok(LockedSource { dir, file: None });
188+
}
186189
}
187190

188-
// Recursively update Git submodules.
189-
git::submodule_update(&repo).context("failed to recursively update submodules")?;
191+
let temp_dir = TempPath::new(&dir);
192+
let repo = git::clone(&url, &temp_dir.path())?;
193+
checkout(repo, "Cloned")?;
194+
temp_dir
195+
.rename(&dir)
196+
.context("failed to rename temporary clone directory")?;
190197

191198
Ok(LockedSource { dir, file: None })
192199
}
193200

194201
/// Downloads a Remote source.
195202
fn lock_remote(ctx: &Context, dir: PathBuf, file: PathBuf, url: Url) -> Result<LockedSource> {
196-
if ctx.reinstall {
197-
if let Err(e) = fs::remove_file(&file) {
198-
if e.kind() != io::ErrorKind::NotFound {
199-
return Err(e).with_context(s!("failed to remove file `{}`", &file.display()));
200-
}
201-
}
202-
}
203-
204-
if file.exists() {
203+
if !ctx.reinstall && file.exists() {
205204
status!(ctx, "Checked", &url);
206-
} else {
207-
fs::create_dir_all(&dir)
208-
.with_context(s!("failed to create dir `{}`", dir.display()))?;
209-
let mut response = reqwest::blocking::get(url.clone())
210-
.with_context(s!("failed to download `{}`", url))?;
211-
let mut out = fs::File::create(&file)
212-
.with_context(s!("failed to create `{}`", file.display()))?;
213-
io::copy(&mut response, &mut out)
214-
.with_context(s!("failed to copy contents to `{}`", file.display()))?;
215-
status!(ctx, "Fetched", &url);
205+
return Ok(LockedSource {
206+
dir,
207+
file: Some(file),
208+
});
216209
}
217210

211+
let mut response =
212+
reqwest::blocking::get(url.clone()).with_context(s!("failed to download `{}`", url))?;
213+
fs::create_dir_all(&dir).with_context(s!("failed to create dir `{}`", dir.display()))?;
214+
let mut temp_file = TempPath::new(&file);
215+
temp_file.write(&mut response).with_context(s!(
216+
"failed to copy contents to `{}`",
217+
temp_file.path().display()
218+
))?;
219+
temp_file
220+
.rename(&file)
221+
.context("failed to rename temporary download file")?;
222+
218223
Ok(LockedSource {
219224
dir,
220225
file: Some(file),
@@ -757,7 +762,7 @@ mod tests {
757762
use super::*;
758763
use std::{
759764
fs,
760-
io::{Read, Write},
765+
io::{self, Read, Write},
761766
process::Command,
762767
thread, time,
763768
};
@@ -920,15 +925,14 @@ mod tests {
920925
thread::sleep(time::Duration::from_secs(1));
921926
ctx.reinstall = true;
922927
let locked = Source::lock_git(&ctx, dir.to_path_buf(), url, None).unwrap();
923-
924928
assert_eq!(locked.dir, dir);
925929
assert_eq!(locked.file, None);
926930
let repo = git2::Repository::open(&dir).unwrap();
927931
assert_eq!(
928932
repo.head().unwrap().target().unwrap().to_string(),
929933
"be8fde277e76f35efbe46848fb352cee68549962"
930934
);
931-
assert!(fs::metadata(&dir).unwrap().modified().unwrap() > modified)
935+
assert!(fs::metadata(&dir).unwrap().modified().unwrap() > modified);
932936
}
933937

934938
#[test]

src/util.rs

Lines changed: 98 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::{
44
fs::{self, File},
55
io,
66
path::{Path, PathBuf},
7-
time,
7+
process, time,
88
};
99

1010
use anyhow::{Context as ResultExt, Error, Result};
@@ -22,6 +22,19 @@ pub fn underlying_io_error_kind(error: &Error) -> Option<io::ErrorKind> {
2222
None
2323
}
2424

25+
/// Remove a file or directory.
26+
fn nuke_path(path: &Path) -> io::Result<()> {
27+
if path.is_dir() {
28+
fs::remove_dir_all(path)
29+
} else {
30+
fs::remove_file(path)
31+
}
32+
}
33+
34+
/////////////////////////////////////////////////////////////////////////
35+
// PathExt trait
36+
/////////////////////////////////////////////////////////////////////////
37+
2538
/// An extension trait for [`Path`] types.
2639
///
2740
/// [`Path`]: https://doc.rust-lang.org/std/path/struct.Path.html
@@ -84,6 +97,73 @@ impl PathExt for Path {
8497
}
8598
}
8699

100+
/////////////////////////////////////////////////////////////////////////
101+
// TempPath type
102+
/////////////////////////////////////////////////////////////////////////
103+
104+
/// Holds a temporary directory or file path that is removed when dropped.
105+
pub struct TempPath {
106+
/// The temporary directory or file path.
107+
pub path: Option<PathBuf>,
108+
}
109+
110+
impl TempPath {
111+
/// Create a new `TempPath`.
112+
pub fn new(original_path: &Path) -> Self {
113+
let mut path = original_path.parent().unwrap().to_path_buf();
114+
let mut file_name = original_path.file_stem().unwrap().to_os_string();
115+
file_name.push(format!("-tmp-{}", process::id()));
116+
if let Some(ext) = original_path.extension() {
117+
file_name.push(".");
118+
file_name.push(ext);
119+
}
120+
path.push(file_name);
121+
Self { path: Some(path) }
122+
}
123+
124+
/// Access the underlying `Path`.
125+
pub fn path(&self) -> &Path {
126+
self.path.as_ref().unwrap()
127+
}
128+
129+
/// Copy the contents of a stream to this `TempPath`.
130+
pub fn write<R>(&mut self, mut reader: &mut R) -> io::Result<()>
131+
where
132+
R: io::Read,
133+
{
134+
let mut file = fs::File::create(self.path())?;
135+
io::copy(&mut reader, &mut file)?;
136+
Ok(())
137+
}
138+
139+
/// Move the temporary path to a new location.
140+
pub fn rename(mut self, new_path: &Path) -> io::Result<()> {
141+
if let Err(err) = nuke_path(new_path) {
142+
if err.kind() != io::ErrorKind::NotFound {
143+
return Err(err);
144+
}
145+
};
146+
if let Some(path) = &self.path {
147+
fs::rename(path, new_path)?;
148+
// This is so that the Drop impl doesn't try delete a non-existent file.
149+
self.path = None;
150+
}
151+
Ok(())
152+
}
153+
}
154+
155+
impl Drop for TempPath {
156+
fn drop(&mut self) {
157+
if let Some(path) = &self.path {
158+
nuke_path(&path).ok();
159+
}
160+
}
161+
}
162+
163+
/////////////////////////////////////////////////////////////////////////
164+
// Mutex type
165+
/////////////////////////////////////////////////////////////////////////
166+
87167
#[derive(Debug)]
88168
pub struct Mutex(File);
89169

@@ -123,11 +203,15 @@ impl Drop for Mutex {
123203
}
124204
}
125205

206+
/////////////////////////////////////////////////////////////////////////
207+
// Git module
208+
/////////////////////////////////////////////////////////////////////////
209+
126210
pub mod git {
127211
use std::path::Path;
128212

129213
use git2::{
130-
build::RepoBuilder, BranchType, Cred, CredentialType, Error, ErrorCode, FetchOptions, Oid,
214+
build::RepoBuilder, BranchType, Cred, CredentialType, Error, FetchOptions, Oid,
131215
RemoteCallbacks, Repository, ResetType,
132216
};
133217
use url::Url;
@@ -158,28 +242,21 @@ pub mod git {
158242
f(opts)
159243
}
160244

161-
/// Clone or open a Git repository.
162-
pub fn clone_or_open(url: &Url, dir: &Path) -> anyhow::Result<(bool, Repository)> {
245+
/// Open a Git repository.
246+
pub fn open(dir: &Path) -> anyhow::Result<Repository> {
247+
let repo = Repository::open(dir)
248+
.with_context(s!("failed to open repository at `{}`", dir.display()))?;
249+
Ok(repo)
250+
}
251+
252+
/// Clone a Git repository.
253+
pub fn clone(url: &Url, dir: &Path) -> anyhow::Result<Repository> {
163254
with_fetch_options(|opts| {
164-
let mut cloned = false;
165-
let repo = match RepoBuilder::new()
255+
let repo = RepoBuilder::new()
166256
.fetch_options(opts)
167257
.clone(url.as_str(), dir)
168-
{
169-
Ok(repo) => {
170-
cloned = true;
171-
repo
172-
}
173-
Err(e) => {
174-
if e.code() == ErrorCode::Exists {
175-
Repository::open(dir)
176-
.with_context(s!("failed to open repository at `{}`", dir.display()))?
177-
} else {
178-
return Err(e).with_context(s!("failed to git clone `{}`", url));
179-
}
180-
}
181-
};
182-
Ok((cloned, repo))
258+
.with_context(s!("failed to git clone `{}`", url))?;
259+
Ok(repo)
183260
})
184261
}
185262

0 commit comments

Comments
 (0)