Coverage for openhcs/core/xdg_paths.py: 22.6%

103 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 05:57 +0000

1""" 

2XDG Base Directory Specification utilities for OpenHCS. 

3 

4Provides standardized paths following XDG Base Directory Specification: 

5- Data: ~/.local/share/openhcs/ 

6- Cache: ~/.local/share/openhcs/cache/ 

7- Config: ~/.local/share/openhcs/config/ 

8 

9Includes migration utilities to move existing cache files from legacy ~/.openhcs/ location. 

10""" 

11 

12import logging 

13import shutil 

14from pathlib import Path 

15from typing import Optional 

16 

17logger = logging.getLogger(__name__) 

18 

19 

20def get_openhcs_data_dir() -> Path: 

21 """ 

22 Get the OpenHCS data directory following XDG Base Directory Specification. 

23  

24 Returns: 

25 Path to ~/.local/share/openhcs/ 

26 """ 

27 data_dir = Path.home() / ".local" / "share" / "openhcs" 

28 data_dir.mkdir(parents=True, exist_ok=True) 

29 return data_dir 

30 

31 

32def get_openhcs_cache_dir() -> Path: 

33 """ 

34 Get the OpenHCS cache directory following XDG Base Directory Specification. 

35  

36 Returns: 

37 Path to ~/.local/share/openhcs/cache/ 

38 """ 

39 cache_dir = get_openhcs_data_dir() / "cache" 

40 cache_dir.mkdir(parents=True, exist_ok=True) 

41 return cache_dir 

42 

43 

44def get_openhcs_config_dir() -> Path: 

45 """ 

46 Get the OpenHCS config directory following XDG Base Directory Specification. 

47  

48 Returns: 

49 Path to ~/.local/share/openhcs/config/ 

50 """ 

51 config_dir = get_openhcs_data_dir() / "config" 

52 config_dir.mkdir(parents=True, exist_ok=True) 

53 return config_dir 

54 

55 

56def get_legacy_openhcs_dir() -> Path: 

57 """ 

58 Get the legacy OpenHCS directory (~/.openhcs/). 

59  

60 Returns: 

61 Path to ~/.openhcs/ 

62 """ 

63 return Path.home() / ".openhcs" 

64 

65 

66def migrate_cache_file(legacy_path: Path, new_path: Path, description: str) -> bool: 

67 """ 

68 Migrate a cache file from legacy location to XDG-compliant location. 

69  

70 Args: 

71 legacy_path: Path to the legacy cache file 

72 new_path: Path to the new XDG-compliant location 

73 description: Human-readable description of the cache file 

74  

75 Returns: 

76 True if migration was performed, False if no migration needed 

77 """ 

78 if not legacy_path.exists(): 

79 logger.debug(f"No legacy {description} found at {legacy_path}") 

80 return False 

81 

82 if new_path.exists(): 

83 logger.debug(f"XDG {description} already exists at {new_path}, skipping migration") 

84 return False 

85 

86 try: 

87 # Ensure target directory exists 

88 new_path.parent.mkdir(parents=True, exist_ok=True) 

89 

90 # Copy the file to new location 

91 shutil.copy2(legacy_path, new_path) 

92 logger.info(f"Migrated {description} from {legacy_path} to {new_path}") 

93 

94 # Remove the legacy file 

95 legacy_path.unlink() 

96 logger.debug(f"Removed legacy {description} at {legacy_path}") 

97 

98 return True 

99 

100 except Exception as e: 

101 logger.warning(f"Failed to migrate {description} from {legacy_path} to {new_path}: {e}") 

102 return False 

103 

104 

105def migrate_cache_directory(legacy_dir: Path, new_dir: Path, description: str) -> bool: 

106 """ 

107 Migrate an entire cache directory from legacy location to XDG-compliant location. 

108  

109 Args: 

110 legacy_dir: Path to the legacy cache directory 

111 new_dir: Path to the new XDG-compliant directory 

112 description: Human-readable description of the cache directory 

113  

114 Returns: 

115 True if migration was performed, False if no migration needed 

116 """ 

117 if not legacy_dir.exists(): 

118 logger.debug(f"No legacy {description} found at {legacy_dir}") 

119 return False 

120 

121 if new_dir.exists() and any(new_dir.iterdir()): 

122 logger.debug(f"XDG {description} already exists and is not empty at {new_dir}, skipping migration") 

123 return False 

124 

125 try: 

126 # Ensure target parent directory exists 

127 new_dir.parent.mkdir(parents=True, exist_ok=True) 

128 

129 # Copy the entire directory to new location 

130 if new_dir.exists(): 

131 shutil.rmtree(new_dir) 

132 shutil.copytree(legacy_dir, new_dir) 

133 logger.info(f"Migrated {description} from {legacy_dir} to {new_dir}") 

134 

135 # Remove the legacy directory 

136 shutil.rmtree(legacy_dir) 

137 logger.debug(f"Removed legacy {description} at {legacy_dir}") 

138 

139 return True 

140 

141 except Exception as e: 

142 logger.warning(f"Failed to migrate {description} from {legacy_dir} to {new_dir}: {e}") 

143 return False 

144 

145 

146def migrate_all_legacy_cache_files() -> None: 

147 """ 

148 Migrate all known legacy cache files to XDG-compliant locations. 

149  

150 This function should be called during application startup to ensure 

151 smooth transition from legacy cache locations. 

152 """ 

153 legacy_dir = get_legacy_openhcs_dir() 

154 

155 if not legacy_dir.exists(): 

156 logger.debug("No legacy ~/.openhcs directory found, no migration needed") 

157 return 

158 

159 logger.info("Checking for legacy cache files to migrate to XDG locations...") 

160 

161 # Migrate individual cache files 

162 migrations = [ 

163 (legacy_dir / "path_cache.json", get_openhcs_data_dir() / "path_cache.json", "path cache"), 

164 (legacy_dir / "global_config.config", get_openhcs_config_dir() / "global_config.config", "global config cache"), 

165 ] 

166 

167 migrated_files = 0 

168 for legacy_path, new_path, description in migrations: 

169 if migrate_cache_file(legacy_path, new_path, description): 

170 migrated_files += 1 

171 

172 # Migrate cache directory 

173 legacy_cache_dir = legacy_dir / "cache" 

174 new_cache_dir = get_openhcs_cache_dir() 

175 if migrate_cache_directory(legacy_cache_dir, new_cache_dir, "cache directory"): 

176 migrated_files += 1 

177 

178 # Clean up empty legacy directory 

179 try: 

180 if legacy_dir.exists() and not any(legacy_dir.iterdir()): 

181 legacy_dir.rmdir() 

182 logger.info(f"Removed empty legacy directory {legacy_dir}") 

183 except Exception as e: 

184 logger.debug(f"Could not remove legacy directory {legacy_dir}: {e}") 

185 

186 if migrated_files > 0: 

187 logger.info(f"Successfully migrated {migrated_files} cache files/directories to XDG locations") 

188 else: 

189 logger.debug("No legacy cache files found to migrate") 

190 

191 

192def get_cache_file_path(filename: str, legacy_filename: Optional[str] = None) -> Path: 

193 """ 

194 Get path for a cache file, with automatic migration from legacy location. 

195  

196 Args: 

197 filename: Name of the cache file in XDG location 

198 legacy_filename: Optional different filename in legacy location 

199  

200 Returns: 

201 Path to the cache file in XDG location 

202 """ 

203 new_path = get_openhcs_cache_dir() / filename 

204 

205 # Check if we need to migrate from legacy location 

206 if not new_path.exists(): 

207 legacy_filename = legacy_filename or filename 

208 legacy_path = get_legacy_openhcs_dir() / "cache" / legacy_filename 

209 

210 if legacy_path.exists(): 210 ↛ 211line 210 didn't jump to line 211 because the condition on line 210 was never true

211 migrate_cache_file(legacy_path, new_path, f"cache file {filename}") 

212 

213 return new_path 

214 

215 

216def get_config_file_path(filename: str, legacy_filename: Optional[str] = None) -> Path: 

217 """ 

218 Get path for a config file, with automatic migration from legacy location. 

219  

220 Args: 

221 filename: Name of the config file in XDG location 

222 legacy_filename: Optional different filename in legacy location 

223  

224 Returns: 

225 Path to the config file in XDG location 

226 """ 

227 new_path = get_openhcs_config_dir() / filename 

228 

229 # Check if we need to migrate from legacy location 

230 if not new_path.exists(): 

231 legacy_filename = legacy_filename or filename 

232 legacy_path = get_legacy_openhcs_dir() / legacy_filename 

233 

234 if legacy_path.exists(): 

235 migrate_cache_file(legacy_path, new_path, f"config file {filename}") 

236 

237 return new_path 

238 

239 

240def get_data_file_path(filename: str, legacy_filename: Optional[str] = None) -> Path: 

241 """ 

242 Get path for a data file, with automatic migration from legacy location. 

243  

244 Args: 

245 filename: Name of the data file in XDG location 

246 legacy_filename: Optional different filename in legacy location 

247  

248 Returns: 

249 Path to the data file in XDG location 

250 """ 

251 new_path = get_openhcs_data_dir() / filename 

252 

253 # Check if we need to migrate from legacy location 

254 if not new_path.exists(): 

255 legacy_filename = legacy_filename or filename 

256 legacy_path = get_legacy_openhcs_dir() / legacy_filename 

257 

258 if legacy_path.exists(): 

259 migrate_cache_file(legacy_path, new_path, f"data file {filename}") 

260 

261 return new_path