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 

2 

3:copyright: Copyright 2020 Edward Armitage. 

4:license: MIT, see LICENSE for details. 

5""" 

6import os 

7import shutil 

8from datetime import date 

9from pathlib import Path 

10from typing import List, Generator 

11 

12from photoimport.ui import ConsoleWriter 

13 

14 

15class FileMover: 

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

17 

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 

31 

32 if self._dry_run_mode: 

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

34 

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

36 """Creates a directory for the given date 

37 

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. 

42 

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

44 """ 

45 directory = self._build_path(folder_date) 

46 

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) 

51 

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

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

54 

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

56 mode. 

57 

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) 

65 

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}" 

70 

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) 

75 

76 return path 

77 

78 

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

80 """Finds all photos at the given path 

81 

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. 

85 

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

87 allows this. 

88 

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 """ 

93 

94 def is_photo(file_path: Path): 

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

96 

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

98 the file has a ".jpg" extension. 

99 

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

101 

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}") 

108 

109 return result 

110 

111 if is_photo(source): 

112 yield source 

113 return 

114 

115 if os.path.isdir(source): 

116 for file in source.iterdir(): 

117 if not is_photo(file): 

118 continue 

119 

120 yield file 

121 

122 

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

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

125 

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. 

129 

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}.*"))