Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1"""All the functionality to do with the filesystem 


3:copyright: Copyright 2020 Edward Armitage. 

4:license: MIT, see LICENSE for details. 


6import os 

7import shutil 

8from datetime import date 

9from pathlib import Path 

10from typing import List, Generator 


12from photoimport.ui import ConsoleWriter 



15class FileMover: 

16 """Class for moving files into a date-organised hierarchy""" 


18 def __init__(self, writer: ConsoleWriter, base_path: str, dry_run_mode: bool, month_only: bool) -> None: 

19 """ 

20 :param writer: The ConsoleWriter used to inform of progress 

21 :param base_path: The directory to build the date-organised hierarchy within 

22 :param dry_run_mode When true, no folders will be created on the filesystem and no files will 

23 be actually moved 

24 :param month_only: Whether to store all files for a given month in the same folder, rather 

25 than separating by day (default is False) 

26 """ 

27 self._writer = writer 

28 self._base_path = Path(base_path) 

29 self._dry_run_mode = dry_run_mode 

30 self._month_only = month_only 


32 if self._dry_run_mode: 

33 self._writer.status("In dry-run mode. No folders will be created or files moved.") 


35 def create_directory(self, folder_date: date) -> None: 

36 """Creates a directory for the given date 


38 If the required folder does not already exist, it will be created (if not in dry-run 

39 mode) and log when not in silent mode. If a top-level folder already exists (e.g. the 

40 year or month folder), the lower level (e.g. month or date) folders will be created 

41 alongside any existing files. 


43 :param folder_date: The date of the folder to be created 

44 """ 

45 directory = self._build_path(folder_date) 


47 if not os.path.isdir(directory): 

48 self._writer.action(f"📂 Creating {directory}") 

49 if not self._dry_run_mode: 

50 os.makedirs(directory, exist_ok=True) 


52 def move_file(self, file: Path, folder_date: date) -> None: 

53 """Moves a file into an appropriate directory for the given date 


55 Will only move files if not in dry-run mode. Will log when not in silent 

56 mode. 


58 :param file: The file to be moved 

59 :param folder_date: The date of the folder to move the file into 

60 """ 

61 directory = self._build_path(folder_date) 

62 self._writer.action(f"➡️ Moving {file} to {directory}") 

63 if not self._dry_run_mode: 

64 shutil.move(str(file), directory) 


66 def _build_path(self, folder_date: date) -> str: 

67 year = f"{folder_date.year:04d}" 

68 month = f"{folder_date.month:02d}" 

69 day = f"{folder_date.day:02d}" 


71 if self._month_only: 

72 path = os.path.join(self._base_path, year, month) 

73 else: 

74 path = os.path.join(self._base_path, year, month, day) 


76 return path 



79def find_all_photos(source: Path, writer: ConsoleWriter) -> Generator[Path, None, None]: 

80 """Finds all photos at the given path 


82 If the source path is the path of a photo, then this is the photo that will be 

83 imported; alternatively if the source path is of a directory containing photos, 

84 then the photos within this directory will be imported. 


86 Any non-photo files will be ignored, but this will be logged if current config 

87 allows this. 


89 :param source: the Path where photos should be found 

90 :param writer: the writer used to output any information 

91 :return: a generator yielding paths to the photos within source 

92 """ 


94 def is_photo(file_path: Path): 

95 """Determine if a file_path points at a photo or not 


97 This is determined by checking that the provided path is to a file, and that 

98 the file has a ".jpg" extension. 


100 Logs a warning if the photo is not deemed to be a photo. 


102 :param file_path: the path to be checked 

103 :return: True if the path points to a photo; otherwise False 

104 """ 

105 result = os.path.isfile(file_path) and file_path.suffix == ".jpg" 

106 if not result: 

107 writer.status(f"⚠️ Ignoring {file_path.name}") 


109 return result 


111 if is_photo(source): 

112 yield source 

113 return 


115 if os.path.isdir(source): 

116 for file in source.iterdir(): 

117 if not is_photo(file): 

118 continue 


120 yield file 



123def find_companion_files(file: Path) -> List[Path]: 

124 """Finds the companion files alongside the provided the file in a given directory 


126 A companion file is a file within the same directory as the original file, with the same 

127 name (ignoring file extensions). For example photo-001.raw is a companion to photo-001.jpg, 

128 but photo-0011.jpg is not. 


130 :param file: The file to find companions for 

131 :return: a list of Paths containing the provided file and all companion files 

132 """ 

133 return list(file.parent.glob(f"{file.stem}.*"))