gphoto2/
filesys.rs

1//! Camera filesystem and storages
2
3use crate::{
4  file::{CameraFile, FileType},
5  helper::{bitflags, char_slice_to_cow, to_c_string, UninitBox},
6  list::{CameraList, FileListIter},
7  task::Task,
8  try_gp_internal, Camera, Error, Result,
9};
10use libgphoto2_sys::time_t;
11use std::{borrow::Cow, ffi, fmt, fs, path::Path};
12
13macro_rules! storage_info {
14  ($(# $attr:tt)* $name:ident: $bitflag_ty:ident, |$inner:ident: $inner_ty:ident| { $($(# $field_attr:tt)* $field:ident: $ty:ty = $bitflag:ident, $expr:expr;)* }) => {
15    $(# $attr)*
16    #[repr(transparent)]
17    #[derive(Clone)]
18    pub struct $name {
19      inner: libgphoto2_sys::$inner_ty,
20    }
21
22    impl $name {
23      #[allow(dead_code)]
24      pub(crate) fn from_inner_ref(ptr: &libgphoto2_sys::$inner_ty) -> &Self {
25        let ptr: *const _ = ptr;
26        // Safe because of repr(transparent).
27        unsafe { &*ptr.cast::<Self>() }
28      }
29
30      $(
31        $(# $field_attr)*
32        pub fn $field(&self) -> Option<$ty> {
33          let $inner = &self.inner;
34          if ($inner.fields & libgphoto2_sys::$bitflag_ty::$bitflag).0 != 0 {
35            Some($expr)
36          } else {
37            None
38          }
39        }
40      )*
41    }
42
43    impl fmt::Debug for $name {
44      fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        f.debug_struct(stringify!($name))
46          $(
47            .field(stringify!($field), &self.$field())
48          )*
49          .finish()
50      }
51    }
52  };
53}
54
55/// Hardware storage type
56#[derive(Debug, Hash, PartialEq, Eq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize))]
58pub enum StorageType {
59  /// Unknown storage type
60  Unknown,
61  /// Fixed ROM storage
62  FixedRom,
63  /// Removable ROM storage
64  RemovableRom,
65  /// Fixed RAM
66  FixedRam,
67  /// Removable RAM storage (sd cards)
68  RemovableRam,
69}
70
71/// Type of the filesystem hierarchy
72#[derive(Debug, Hash, PartialEq, Eq)]
73#[cfg_attr(feature = "serde", derive(serde::Serialize))]
74pub enum FilesystemType {
75  /// Unknown filesystem type
76  Unknown,
77  /// Flat filesystem (all in one directory)
78  Flat,
79  /// Tree hierarchy
80  Tree,
81  /// DCIM style filesystem
82  Dcf,
83}
84
85/// Access types of storage
86#[derive(Debug, Hash, PartialEq, Eq)]
87#[cfg_attr(feature = "serde", derive(serde::Serialize))]
88pub enum AccessType {
89  /// Read/Write
90  Rw,
91  /// Read only
92  Ro,
93  /// Read only with delete
94  RoDelete,
95}
96
97bitflags!(
98  /// Status of [`CameraFile`].
99  FileStatus = CameraFileStatus {
100    /// This file has been downloaded.
101    downloaded: GP_FILE_STATUS_DOWNLOADED,
102  }
103);
104
105bitflags!(
106  /// Permissions of a [`CameraFile`]
107  FilePermissions = CameraFilePermissions {
108    /// File can be read
109    read: GP_FILE_PERM_READ,
110
111    /// File can be deleted
112    delete: GP_FILE_PERM_DELETE,
113  }
114);
115
116storage_info!(
117  /// Image thumbnail information
118  FileInfoPreview: CameraFileInfoFields, |info: CameraFileInfoPreview| {
119    /// Status of the preview file
120    status: FileStatus = GP_FILE_INFO_STATUS, info.status.into();
121    /// Size of the preview file
122    size: u64 = GP_FILE_INFO_SIZE, info.size;
123    /// Mime type of the preview file
124    mime_type: Cow<str> = GP_FILE_INFO_TYPE, char_slice_to_cow(&info.type_);
125    /// Image width,
126    width: u32 = GP_FILE_INFO_WIDTH, info.width;
127    /// Image height
128    height: u32 = GP_FILE_INFO_HEIGHT, info.height;
129  }
130);
131
132storage_info!(
133  /// Info for image file
134  FileInfoFile: CameraFileInfoFields, |info: CameraFileInfoFile| {
135    /// Status of the file
136    status: FileStatus = GP_FILE_INFO_STATUS, info.status.into();
137    /// File size
138    size: u64 = GP_FILE_INFO_SIZE, info.size;
139    /// Mime type
140    mime_type: Cow<str> = GP_FILE_INFO_TYPE, char_slice_to_cow(&info.type_);
141    /// Image width
142    width: u32 = GP_FILE_INFO_WIDTH, info.width;
143    /// Image height
144    height: u32 = GP_FILE_INFO_HEIGHT, info.height;
145    /// Image permissions
146    permissions: FilePermissions = GP_FILE_INFO_PERMISSIONS, info.permissions.into();
147    /// File modification time
148    mtime: time_t = GP_FILE_INFO_MTIME, info.mtime;
149  }
150);
151
152storage_info!(
153  /// Info for file audio data
154  FileInfoAudio: CameraFileInfoFields, |info: CameraFileInfoAudio| {
155    /// Status of the audio file
156    status: FileStatus = GP_FILE_INFO_STATUS, info.status.into();
157    /// Size of the audio file
158    size: u64 = GP_FILE_INFO_SIZE, info.size;
159    /// Mime type of the audio
160    mime_type: Cow<str> = GP_FILE_INFO_TYPE, char_slice_to_cow(&info.type_);
161  }
162);
163
164/// File information for preview, normal file and audio
165pub struct FileInfo {
166  // It's fairly large, so we want to keep it on the heap.
167  pub(crate) inner: Box<libgphoto2_sys::CameraFileInfo>,
168}
169
170impl FileInfo {
171  /// Info for file preview
172  pub fn preview(&self) -> &FileInfoPreview {
173    FileInfoPreview::from_inner_ref(&self.inner.preview)
174  }
175
176  /// Info for normal file
177  pub fn file(&self) -> &FileInfoFile {
178    FileInfoFile::from_inner_ref(&self.inner.file)
179  }
180
181  /// Info for file audio
182  pub fn audio(&self) -> &FileInfoAudio {
183    FileInfoAudio::from_inner_ref(&self.inner.audio)
184  }
185}
186
187impl fmt::Debug for FileInfo {
188  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189    f.debug_struct("FileInfo")
190      .field("preview", &self.preview())
191      .field("file", &self.file())
192      .field("audio", &self.audio())
193      .finish()
194  }
195}
196
197/// File system actions for a camera
198pub struct CameraFS<'a> {
199  pub(crate) camera: &'a Camera,
200}
201
202impl From<libgphoto2_sys::CameraStorageType> for StorageType {
203  fn from(storage_type: libgphoto2_sys::CameraStorageType) -> Self {
204    use libgphoto2_sys::CameraStorageType;
205
206    match storage_type {
207      CameraStorageType::GP_STORAGEINFO_ST_UNKNOWN => Self::Unknown,
208      CameraStorageType::GP_STORAGEINFO_ST_FIXED_ROM => Self::FixedRom,
209      CameraStorageType::GP_STORAGEINFO_ST_REMOVABLE_ROM => Self::RemovableRom,
210      CameraStorageType::GP_STORAGEINFO_ST_FIXED_RAM => Self::FixedRam,
211      CameraStorageType::GP_STORAGEINFO_ST_REMOVABLE_RAM => Self::RemovableRam,
212    }
213  }
214}
215
216impl From<libgphoto2_sys::CameraStorageFilesystemType> for FilesystemType {
217  fn from(fs_type: libgphoto2_sys::CameraStorageFilesystemType) -> Self {
218    use libgphoto2_sys::CameraStorageFilesystemType as GPFsType;
219
220    match fs_type {
221      GPFsType::GP_STORAGEINFO_FST_UNDEFINED => Self::Unknown,
222      GPFsType::GP_STORAGEINFO_FST_GENERICFLAT => Self::Flat,
223      GPFsType::GP_STORAGEINFO_FST_GENERICHIERARCHICAL => Self::Tree,
224      GPFsType::GP_STORAGEINFO_FST_DCF => Self::Dcf,
225    }
226  }
227}
228
229impl From<libgphoto2_sys::CameraStorageAccessType> for AccessType {
230  fn from(access_type: libgphoto2_sys::CameraStorageAccessType) -> Self {
231    use libgphoto2_sys::CameraStorageAccessType as GPAccessType;
232
233    match access_type {
234      GPAccessType::GP_STORAGEINFO_AC_READWRITE => Self::Rw,
235      GPAccessType::GP_STORAGEINFO_AC_READONLY => Self::Ro,
236      GPAccessType::GP_STORAGEINFO_AC_READONLY_WITH_DELETE => Self::RoDelete,
237    }
238  }
239}
240
241storage_info!(
242  /// Information about a storage on the camera
243  StorageInfo: CameraStorageInfoFields, |info: CameraStorageInformation| {
244    /// Label of the storage
245    label: Cow<str> = GP_STORAGEINFO_LABEL, char_slice_to_cow(&info.label);
246    /// Base directory of the storage. If there is only 1 storage on the camera it will be "/"
247    base_directory: Cow<str> = GP_STORAGEINFO_BASE, char_slice_to_cow(&info.basedir);
248    /// Description of the storage
249    description: Cow<str> = GP_STORAGEINFO_DESCRIPTION, char_slice_to_cow(&info.description);
250    /// Type of the storage
251    storage_type: StorageType = GP_STORAGEINFO_STORAGETYPE, info.type_.into();
252    /// Type of the filesystem on the storage
253    filesystem_type: FilesystemType = GP_STORAGEINFO_FILESYSTEMTYPE, info.fstype.into();
254    /// Access permissions
255    access_type: AccessType = GP_STORAGEINFO_ACCESS, info.access.into();
256    /// Total storage capacity in Kilobytes
257    capacity_kb: u64 = GP_STORAGEINFO_MAXCAPACITY, info.capacitykbytes * 1024;
258    /// Free storage in Kilobytes
259    free_kb: u64 = GP_STORAGEINFO_FREESPACEKBYTES, info.freekbytes * 1024;
260    /// Number of images that fit in free space (guessed by the camera)
261    free_images: u64 = GP_STORAGEINFO_FREESPACEIMAGES, info.freeimages;
262  }
263);
264
265impl<'a> CameraFS<'a> {
266  pub(crate) fn new(camera: &'a Camera) -> Self {
267    Self { camera }
268  }
269
270  /// Delete a file
271  pub fn delete_file(&self, folder: &str, file: &str) -> Task<Result<()>> {
272    let camera = self.camera.camera;
273    let context = self.camera.context.inner;
274    let (folder, file) = (folder.to_owned(), file.to_owned());
275
276    unsafe {
277      Task::new(move || {
278        try_gp_internal!(gp_camera_file_delete(
279          *camera,
280          to_c_string!(folder),
281          to_c_string!(file),
282          *context
283        )?);
284        Ok(())
285      })
286    }
287    .context(context)
288  }
289
290  /// Get information of a file
291  pub fn file_info(&self, folder: &str, file: &str) -> Task<Result<FileInfo>> {
292    let camera = self.camera.camera;
293    let context = self.camera.context.inner;
294    let (folder, file) = (folder.to_owned(), file.to_owned());
295
296    unsafe {
297      Task::new(move || {
298        let mut inner = UninitBox::uninit();
299
300        try_gp_internal!(gp_camera_file_get_info(
301          *camera,
302          to_c_string!(folder),
303          to_c_string!(file),
304          inner.as_mut_ptr(),
305          *context
306        )?);
307
308        Ok(FileInfo { inner: inner.assume_init() })
309      })
310    }
311    .context(context)
312  }
313
314  /// Downloads a file from the camera
315  pub fn download_to(&self, folder: &str, file: &str, path: &Path) -> Task<Result<CameraFile>> {
316    self.to_camera_file(folder, file, FileType::Normal, Some(path))
317  }
318
319  /// Downloads a camera file to memory
320  pub fn download(&self, folder: &str, file: &str) -> Task<Result<CameraFile>> {
321    self.to_camera_file(folder, file, FileType::Normal, None)
322  }
323
324  /// Downloads a preview into memory
325  pub fn download_preview(&self, folder: &str, file: &str) -> Task<Result<CameraFile>> {
326    self.to_camera_file(folder, file, FileType::Preview, None)
327  }
328
329  /// Downloads the EXIF block into memory
330  pub fn download_exif(&self, folder: &str, file: &str) -> Task<Result<CameraFile>> {
331    self.to_camera_file(folder, file, FileType::Exif, None)
332  }
333
334  /// Upload a file to the camera
335  #[allow(clippy::boxed_local)]
336  pub fn upload_file(&self, folder: &str, filename: &str, data: Box<[u8]>) -> Task<Result<()>> {
337    let camera = self.camera.camera;
338    let context = self.camera.context.inner;
339
340    let (folder, filename) = (folder.to_owned(), filename.to_owned());
341
342    unsafe {
343      Task::new(move || {
344        try_gp_internal!(gp_file_new(&out file)?);
345        try_gp_internal!(gp_file_append(file, data.as_ptr().cast(), data.len().try_into()?)?);
346        try_gp_internal!(gp_camera_folder_put_file(
347          *camera,
348          to_c_string!(folder),
349          to_c_string!(filename),
350          FileType::Normal.into(),
351          file,
352          *context
353        )?);
354
355        Ok(())
356      })
357    }
358    .context(context)
359  }
360
361  /// Delete all files in a folder
362  pub fn delete_all_in_folder(&self, folder: &str) -> Task<Result<()>> {
363    let camera = self.camera.camera;
364    let context = self.camera.context.inner;
365    let folder = folder.to_owned();
366
367    unsafe {
368      Task::new(move || {
369        try_gp_internal!(gp_camera_folder_delete_all(*camera, to_c_string!(folder), *context)?);
370        Ok(())
371      })
372    }
373    .context(context)
374  }
375
376  /// List files in a folder
377  pub fn list_files(&self, folder: &str) -> Task<Result<FileListIter>> {
378    let camera = self.camera.camera;
379    let context = self.camera.context.inner;
380
381    let folder = folder.to_owned();
382
383    unsafe {
384      Task::new(move || {
385        let file_list = CameraList::new()?;
386
387        try_gp_internal!(gp_camera_folder_list_files(
388          *camera,
389          to_c_string!(folder),
390          *file_list.inner,
391          *context
392        )?);
393
394        Ok(FileListIter::new(file_list))
395      })
396    }
397    .context(context)
398  }
399
400  /// List folders in a folder
401  pub fn list_folders(&self, folder: &str) -> Task<Result<FileListIter>> {
402    let camera = self.camera.camera;
403    let context = self.camera.context.inner;
404
405    let folder = folder.to_owned();
406
407    unsafe {
408      Task::new(move || {
409        let folder_list = CameraList::new()?;
410
411        try_gp_internal!(gp_camera_folder_list_folders(
412          *camera,
413          to_c_string!(folder),
414          *folder_list.inner,
415          *context
416        )?);
417
418        Ok(FileListIter::new(folder_list))
419      })
420    }
421    .context(context)
422  }
423
424  /// Creates a new folder
425  pub fn create_directory(&self, parent_folder: &str, new_folder: &str) -> Task<Result<()>> {
426    let (parent_folder, new_folder) = (parent_folder.to_owned(), new_folder.to_owned());
427    let camera = self.camera.camera;
428    let context = self.camera.context.inner;
429
430    unsafe {
431      Task::new(move || {
432        try_gp_internal!(gp_camera_folder_make_dir(
433          *camera,
434          to_c_string!(parent_folder),
435          to_c_string!(new_folder),
436          *context
437        )?);
438
439        Ok(())
440      })
441    }
442    .context(context)
443  }
444
445  /// Removes a folder
446  pub fn remove_directory(&self, parent: &str, to_remove: &str) -> Task<Result<()>> {
447    let (parent, to_remove) = (parent.to_owned(), to_remove.to_owned());
448    let camera = self.camera.camera;
449    let context = self.camera.context.inner;
450
451    unsafe {
452      Task::new(move || {
453        try_gp_internal!(gp_camera_folder_remove_dir(
454          *camera,
455          to_c_string!(parent),
456          to_c_string!(to_remove),
457          *context
458        )?);
459
460        Ok(())
461      })
462    }
463    .context(context)
464  }
465}
466
467/// Private implementations
468impl CameraFS<'_> {
469  fn to_camera_file(
470    &self,
471    folder: &str,
472    file: &str,
473    type_: FileType,
474    path: Option<&Path>,
475  ) -> Task<Result<CameraFile>> {
476    let (folder, file, path) = (folder.to_owned(), file.to_owned(), path.map(ToOwned::to_owned));
477    let camera = self.camera.camera;
478    let context = self.camera.context.inner;
479
480    unsafe {
481      Task::new(move || {
482        let camera_file = match &path {
483          Some(dest_path) => CameraFile::new_file(dest_path)?,
484          None => CameraFile::new()?,
485        };
486
487        try_gp_internal!(gp_camera_file_get(
488          *camera,
489          to_c_string!(folder),
490          to_c_string!(file),
491          type_.into(),
492          *camera_file.inner,
493          *context
494        )
495        .map_err(|e| {
496          if let Some(path) = path {
497            if let Err(error) = fs::remove_file(path) {
498              return Into::<Error>::into(error);
499            }
500          }
501
502          e
503        })?);
504
505        Ok(camera_file)
506      })
507    }
508    .context(context)
509  }
510}
511
512impl AsRef<crate::Context> for &CameraFS<'_> {
513  fn as_ref(&self) -> &crate::Context {
514    &self.camera.context
515  }
516}