1use std::{
2 borrow::Cow,
3 fmt::{self, Display},
4 fs::remove_dir_all,
5 path::{Path, PathBuf},
6};
7
8use crate::DuctExpressionExt;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum LinkType {
12 Hard,
13 Symbolic,
14}
15
16impl Display for LinkType {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::Hard => write!(f, "hard"),
20 Self::Symbolic => write!(f, "symbolic"),
21 }
22 }
23}
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum Clobber {
27 Never,
28 FileOnly,
29 FileOrDirectory,
30}
31
32impl Display for Clobber {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 Self::Never => write!(f, "clobbering disabled"),
36 Self::FileOnly => write!(f, "file clobbering enabled"),
37 Self::FileOrDirectory => write!(f, "file and directory clobbering enabled"),
38 }
39 }
40}
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum TargetStyle {
44 File,
45 Directory,
46}
47
48impl Display for TargetStyle {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 match self {
51 Self::File => write!(f, "file"),
52 Self::Directory => write!(f, "directory"),
53 }
54 }
55}
56
57#[derive(Debug)]
58pub enum ErrorCause {
59 MissingFileName,
60 CommandFailed(std::io::Error),
61 IOError(std::io::Error),
62 SymlinkNotAllowed,
63}
64
65impl Display for ErrorCause {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 match self {
68 Self::MissingFileName => {
69 write!(f, "Neither the source nor target contained a file name.",)
70 }
71 Self::CommandFailed(err) => write!(f, "`ln` command failed: {}", err),
72 Self::IOError(err) => write!(f, "IO error: {}", err),
73 Self::SymlinkNotAllowed => {
74 write!(
75 f,
76 r"
77Creation symbolic link is not allowed for this system.
78
79For Windows 10 or newer:
80You should use developer mode.
81See https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development
82
83For Window 8.1 or older:
84You need `SeCreateSymbolicLinkPrivilege` security policy.
85See https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links"
86 )
87 }
88 }
89 }
90}
91
92#[derive(Debug)]
93pub struct Error {
94 link_type: LinkType,
95 force: Clobber,
96 source: PathBuf,
97 target: PathBuf,
98 target_style: TargetStyle,
99 cause: ErrorCause,
100}
101
102impl Error {
103 pub fn new(
104 link_type: LinkType,
105 force: Clobber,
106 source: PathBuf,
107 target: PathBuf,
108 target_style: TargetStyle,
109 cause: ErrorCause,
110 ) -> Self {
111 Self {
112 link_type,
113 force,
114 source,
115 target,
116 target_style,
117 cause,
118 }
119 }
120}
121
122impl Display for Error {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 write!(
125 f,
126 "Failed to create a {} link from {:?} to {} {:?} ({}): {}",
127 self.link_type, self.source, self.target_style, self.target, self.force, self.cause
128 )
129 }
130}
131
132impl std::error::Error for Error {}
133
134#[derive(Clone, Debug)]
135pub struct Call<'a> {
136 link_type: LinkType,
137 force: Clobber,
138 source: &'a Path,
139 target: &'a Path,
140 target_override: Cow<'a, Path>,
141 target_style: TargetStyle,
142}
143
144impl<'a> Call<'a> {
145 pub fn new(
146 link_type: LinkType,
147 force: Clobber,
148 source: &'a Path,
149 target: &'a Path,
150 target_style: TargetStyle,
151 ) -> Result<Self, Error> {
152 let target_override = if let TargetStyle::Directory = target_style {
153 if let Some(file_name) = source.file_name() {
156 Cow::Owned(target.join(file_name))
157 } else {
158 return Err(Error {
159 link_type,
160 force,
161 source: source.to_owned(),
162 target: target.to_owned(),
163 target_style,
164 cause: ErrorCause::MissingFileName,
165 });
166 }
167 } else {
168 Cow::Borrowed(target)
169 };
170 Ok(Self {
171 link_type,
172 force,
173 source,
174 target,
175 target_override,
176 target_style,
177 })
178 }
179
180 pub fn exec(self) -> Result<(), Error> {
181 let mut args = vec!["-n" ];
182 if let LinkType::Symbolic = self.link_type {
183 args.push("-s");
184 }
185 match self.force {
186 Clobber::FileOnly => {
187 args.push("-f");
188 }
189 Clobber::FileOrDirectory => {
190 if self.target_override.is_dir() {
191 remove_dir_all(self.target)
192 .map_err(|err| self.make_error(ErrorCause::IOError(err)))?;
193 }
194 args.push("-f");
195 }
196 _ => (),
197 }
198 let source = self.source.to_string_lossy();
199 let target_override = self.target_override.as_ref().to_string_lossy();
200 args.push(&source);
201 args.push(&target_override);
202 duct::cmd("ln", args)
203 .dup_stdio()
204 .run()
205 .map_err(|err| self.make_error(ErrorCause::CommandFailed(err)))?;
206 Ok(())
207 }
208
209 fn make_error(&self, cause: ErrorCause) -> Error {
210 Error {
211 link_type: self.link_type,
212 force: self.force,
213 source: self.source.to_owned(),
214 target: self.target.to_owned(),
215 target_style: self.target_style,
216 cause,
217 }
218 }
219}
220
221pub fn force_symlink(
222 source: impl AsRef<Path>,
223 target: impl AsRef<Path>,
224 target_style: TargetStyle,
225) -> Result<(), Error> {
226 Call::new(
227 LinkType::Symbolic,
228 Clobber::FileOrDirectory,
229 source.as_ref(),
230 target.as_ref(),
231 target_style,
232 )?
233 .exec()
234}
235
236pub fn force_symlink_relative(
237 abs_source: impl AsRef<Path>,
238 abs_target: impl AsRef<Path>,
239 target_style: TargetStyle,
240) -> Result<(), Error> {
241 let (abs_source, abs_target) = (abs_source.as_ref(), abs_target.as_ref());
242 let rel_source = super::relativize_path(abs_source, abs_target);
243 if target_style == TargetStyle::Directory && rel_source.file_name().is_none() {
244 if let Some(file_name) = abs_source.file_name() {
245 force_symlink(rel_source, abs_target.join(file_name), TargetStyle::File)
246 } else {
247 Err(Error {
248 link_type: LinkType::Symbolic,
249 force: Clobber::FileOrDirectory,
250 source: rel_source,
251 target: abs_target.to_owned(),
252 target_style,
253 cause: ErrorCause::MissingFileName,
254 })
255 }
256 } else {
257 force_symlink(rel_source, abs_target, target_style)
258 }
259}