1#[cfg(unix)]
4use std::os::unix::fs::PermissionsExt;
5use std::path::{Path, PathBuf};
6
7use tar::Archive;
8use tokio::fs::{File, OpenOptions};
9use uuid::Uuid;
10use xz2::read::XzDecoder;
11use zip::ZipArchive;
12
13use crate::error::{Error, Result};
14
15pub fn try_path_str(path: &Path) -> Result<&str> {
29 path.to_str()
30 .ok_or_else(|| Error::path_validation(path, "Path contains invalid UTF-8"))
31}
32
33pub fn try_extension(path: &Path) -> Result<String> {
47 let ext = path
48 .extension()
49 .ok_or_else(|| Error::path_validation(path, "File has no extension"))?
50 .to_str()
51 .ok_or_else(|| Error::path_validation(path, "Invalid characters in file extension"))?
52 .to_lowercase();
53
54 Ok(ext)
55}
56
57pub fn create_temp_path(file_path: &Path, file_format: &str) -> PathBuf {
68 let parent_dir = file_path.parent().unwrap_or_else(|| Path::new(""));
69 let uuid = Uuid::new_v4();
70
71 if let Some(file_stem) = file_path.file_stem().and_then(|s| s.to_str()) {
72 parent_dir.join(format!("{}_{}_temp.{}", file_stem, uuid, file_format))
73 } else {
74 parent_dir.join(format!("output_{}_temp.{}", uuid, file_format))
75 }
76}
77
78pub fn determine_mime_type(path: &Path) -> String {
88 let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
89
90 match extension.to_lowercase().as_str() {
91 "mp4" => "video/mp4",
92 "webm" => "video/webm",
93 "mp3" => "audio/mpeg",
94 "m4a" => "audio/mp4",
95 "jpg" | "jpeg" => "image/jpeg",
96 "png" => "image/png",
97 "vtt" => "text/vtt",
98 "srt" => "application/x-subrip",
99 "ass" | "ssa" => "text/x-ssa",
100 _ => "application/octet-stream",
101 }
102 .to_string()
103}
104
105pub fn try_name(path: impl Into<PathBuf>) -> Result<String> {
119 let path: PathBuf = path.into();
120
121 let name = path
122 .file_name()
123 .ok_or_else(|| Error::path_validation(&path, "Path has no file name"))?;
124 let name = name
125 .to_str()
126 .ok_or_else(|| Error::path_validation(&path, "File name contains invalid UTF-8"))?;
127
128 Ok(name.to_string())
129}
130
131pub fn try_without_extension(path: impl Into<PathBuf>) -> Result<String> {
145 let path: PathBuf = path.into();
146
147 let name = path
148 .file_stem()
149 .ok_or_else(|| Error::path_validation(&path, "Path has no file stem"))?;
150 let name = name
151 .to_str()
152 .ok_or_else(|| Error::path_validation(&path, "File stem contains invalid UTF-8"))?;
153
154 Ok(name.to_string())
155}
156
157pub fn try_parent(path: impl Into<PathBuf>) -> Result<PathBuf> {
171 let path: PathBuf = path.into();
172
173 let parent = path
174 .parent()
175 .ok_or_else(|| Error::path_validation(&path, "Path has no parent directory"))?;
176
177 Ok(parent.to_path_buf())
178}
179
180pub async fn create_file(destination: impl Into<PathBuf>) -> Result<File> {
194 let destination: PathBuf = destination.into();
195
196 tracing::debug!(
197 destination = ?destination,
198 "⚙️ Creating new file"
199 );
200
201 let mut open_options = OpenOptions::new();
202 open_options.read(true);
203 open_options.write(true);
204 open_options.create(true);
205 open_options.truncate(true);
206
207 #[cfg(unix)]
208 {
209 open_options.mode(0o644);
210 }
211
212 let file = open_options.open(&destination).await?;
213
214 tracing::debug!(
215 destination = ?destination,
216 "✅ File created successfully"
217 );
218
219 Ok(file)
220}
221
222pub async fn create_dir(destination: impl Into<PathBuf>) -> Result<()> {
237 let destination: PathBuf = destination.into();
238
239 tracing::debug!(
240 destination = ?destination,
241 "⚙️ Creating directory"
242 );
243
244 tokio::fs::create_dir_all(&destination).await?;
245
246 tracing::debug!(
247 destination = ?destination,
248 "✅ Directory created successfully"
249 );
250
251 Ok(())
252}
253
254pub async fn create_parent_dir(destination: impl Into<PathBuf>) -> Result<()> {
269 let destination: PathBuf = destination.into();
270
271 tracing::debug!(
272 destination = ?destination,
273 "⚙️ Creating parent directory"
274 );
275
276 if let Some(parent) = destination.parent() {
277 tokio::fs::create_dir_all(parent).await?;
278 } else {
279 tokio::fs::create_dir_all(&destination).await?;
280 }
281
282 tracing::debug!(
283 destination = ?destination,
284 "✅ Parent directory created successfully"
285 );
286
287 Ok(())
288}
289
290pub async fn extract_zip(zip_path: impl Into<PathBuf>, destination: impl Into<PathBuf>) -> Result<()> {
297 let zip_path: PathBuf = zip_path.into();
298 let destination: PathBuf = destination.into();
299
300 tracing::debug!(
301 zip_path = ?zip_path,
302 destination = ?destination,
303 "⚙️ Extracting zip file"
304 );
305
306 let zip_path_for_tracing = zip_path.clone();
307 let destination_for_tracing = destination.clone();
308
309 tokio::task::spawn_blocking(move || {
310 let file = std::fs::File::open(&zip_path).map_err(|e| Error::io_with_path("open zip file", &zip_path, e))?;
311
312 let mut archive = ZipArchive::new(file)?;
313
314 for i in 0..archive.len() {
315 let mut file = archive.by_index(i)?;
316
317 let file_name = file
318 .enclosed_name()
319 .ok_or_else(|| {
320 Error::path_validation(
321 PathBuf::from(format!("zip entry {}", i)),
322 "Zip entry has no valid file name",
323 )
324 })?
325 .to_path_buf();
326
327 let dest_path = destination.join(file_name);
328
329 if file.is_dir() {
330 std::fs::create_dir_all(&dest_path)
331 .map_err(|e| Error::io_with_path("create directory from zip", &dest_path, e))?;
332 } else {
333 if let Some(parent) = dest_path.parent() {
334 std::fs::create_dir_all(parent)
335 .map_err(|e| Error::io_with_path("create parent directory from zip", parent, e))?;
336 }
337
338 let mut outfile = std::fs::File::create(&dest_path)
339 .map_err(|e| Error::io_with_path("create file from zip", &dest_path, e))?;
340
341 std::io::copy(&mut file, &mut outfile)
342 .map_err(|e| Error::io_with_path("copy file content from zip", &dest_path, e))?;
343 }
344
345 #[cfg(unix)]
347 {
348 use std::os::unix::fs::PermissionsExt;
349 if let Some(mode) = file.unix_mode() {
350 std::fs::set_permissions(&dest_path, std::fs::Permissions::from_mode(mode))
351 .map_err(|e| Error::io_with_path("set permissions from zip", &dest_path, e))?;
352 }
353 }
354 }
355
356 Ok::<_, Error>(())
357 })
358 .await
359 .map_err(|e| Error::runtime("extract zip archive", e))??;
360
361 tracing::debug!(
362 zip_path = ?zip_path_for_tracing,
363 destination = ?destination_for_tracing,
364 "✅ Zip file extracted successfully"
365 );
366
367 Ok(())
368}
369
370pub async fn extract_tar_xz(tar_path: impl Into<PathBuf>, destination: impl Into<PathBuf>) -> Result<()> {
377 let tar_path: PathBuf = tar_path.into();
378 let destination: PathBuf = destination.into();
379
380 tracing::debug!(
381 tar_path = ?tar_path,
382 destination = ?destination,
383 "⚙️ Extracting tar.xz file"
384 );
385
386 let tar_path_for_tracing = tar_path.clone();
387 let destination_for_tracing = destination.clone();
388
389 tokio::task::spawn_blocking(move || {
390 let file = std::fs::File::open(&tar_path).map_err(|e| Error::io_with_path("open tar.xz file", &tar_path, e))?;
391
392 let decompressor = XzDecoder::new(file);
393 let mut archive = Archive::new(decompressor);
394
395 archive
396 .unpack(&destination)
397 .map_err(|e| Error::io_with_path("unpack tar.xz archive", &destination, e))?;
398
399 Ok::<_, Error>(())
400 })
401 .await
402 .map_err(|e| Error::runtime("extract tar.xz archive", e))??;
403
404 tracing::debug!(
405 tar_path = ?tar_path_for_tracing,
406 destination = ?destination_for_tracing,
407 "✅ Tar.xz file extracted successfully"
408 );
409
410 Ok(())
411}
412
413#[cfg(unix)]
419pub async fn set_executable(executable: impl Into<PathBuf>) -> Result<()> {
420 let executable: PathBuf = executable.into();
421
422 tracing::debug!(path = ?executable, "⚙️ Setting executable permissions");
423
424 let mut perms = tokio::fs::metadata(&executable).await?.permissions();
425
426 perms.set_mode(0o755);
427 tokio::fs::set_permissions(executable, perms).await?;
428
429 Ok(())
430}
431
432#[cfg(not(unix))]
438pub async fn set_executable(_executable: impl Into<PathBuf>) -> Result<()> {
439 Ok(())
441}
442
443pub fn random_filename(length: usize) -> String {
453 let uuid = Uuid::new_v4().to_string().replace('-', "");
454
455 uuid.chars().take(length).collect()
456}
457
458use std::sync::LazyLock;
459
460use regex::Regex;
461
462static VIDEO_ID_REGEX_1: LazyLock<Regex> =
463 LazyLock::new(|| Regex::new(r"(?:video|audio)-([a-zA-Z0-9_-]{11})").expect("Invalid regex"));
464static VIDEO_ID_REGEX_2: LazyLock<Regex> =
465 LazyLock::new(|| Regex::new(r"([a-zA-Z0-9_-]{11})\.[a-zA-Z0-9]+$").expect("Invalid regex"));
466static VIDEO_ID_REGEX_3: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[a-zA-Z0-9_-]{11}").expect("Invalid regex"));
467
468pub fn extract_video_id(filename: &str) -> Option<String> {
470 if let Some(captures) = VIDEO_ID_REGEX_1.captures(filename)
472 && let Some(id) = captures.get(1)
473 {
474 return Some(id.as_str().to_string());
475 }
476
477 if let Some(captures) = VIDEO_ID_REGEX_2.captures(filename)
479 && let Some(id) = captures.get(1)
480 {
481 return Some(id.as_str().to_string());
482 }
483
484 if let Some(captures) = VIDEO_ID_REGEX_3.captures(filename)
486 && let Some(id) = captures.get(0)
487 {
488 let id_str = id.as_str();
489 if id_str.len() == 11 {
490 return Some(id_str.to_string());
491 }
492 }
493
494 None
495}
496
497pub async fn remove_temp_file(file_path: impl Into<PathBuf>) -> bool {
508 let file_path: PathBuf = file_path.into();
509 let result = tokio::fs::remove_file(&file_path).await;
510
511 if let Err(ref e) = result {
512 tracing::warn!(path = ?file_path, error = %e, "Failed to remove temporary file");
513 }
514
515 result.is_ok()
516}