Coverage for models / lox_backup_log.py: 32%

120 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-28 01:16 +0000

1# -*- coding: utf-8 -*- 

2 

3from odoo import models, fields, api, _ 

4from odoo.exceptions import UserError 

5import logging 

6 

7_logger = logging.getLogger(__name__) 

8 

9 

10class LoxBackupLog(models.Model): 

11 _name = 'lox.backup.log' 

12 _description = 'LOX Backup Log' 

13 _order = 'create_date desc' 

14 _rec_name = 'display_name' 

15 

16 config_id = fields.Many2one( 

17 'lox.backup.config', 

18 string='Configuration', 

19 required=True, 

20 ondelete='cascade', 

21 ) 

22 schedule_id = fields.Many2one( 

23 'lox.backup.schedule', 

24 string='Schedule', 

25 ondelete='set null', 

26 ) 

27 backup_uuid = fields.Char( 

28 string='Backup UUID', 

29 readonly=True, 

30 index=True, 

31 ) 

32 display_name = fields.Char( 

33 string='Name', 

34 compute='_compute_display_name', 

35 store=True, 

36 ) 

37 

38 component = fields.Selection([ 

39 ('full', 'Full Backup'), 

40 ('database', 'Database Only'), 

41 ('filestore', 'Filestore Only'), 

42 ], string='Component', default='full') 

43 

44 status = fields.Selection([ 

45 ('pending', 'Pending'), 

46 ('in_progress', 'In Progress'), 

47 ('validating', 'Validating'), 

48 ('completed', 'Completed'), 

49 ('failed', 'Failed'), 

50 ('cancelled', 'Cancelled'), 

51 ], string='Status', default='pending', required=True, index=True) 

52 

53 size_bytes = fields.Float( 

54 string='Size (bytes)', 

55 readonly=True, 

56 ) 

57 size_display = fields.Char( 

58 string='Size', 

59 compute='_compute_size_display', 

60 ) 

61 

62 started_at = fields.Datetime( 

63 string='Started At', 

64 readonly=True, 

65 ) 

66 completed_at = fields.Datetime( 

67 string='Completed At', 

68 readonly=True, 

69 ) 

70 duration = fields.Float( 

71 string='Duration (s)', 

72 compute='_compute_duration', 

73 store=True, 

74 ) 

75 

76 error_message = fields.Text( 

77 string='Error Message', 

78 readonly=True, 

79 ) 

80 

81 # Storage info 

82 storage_path = fields.Char( 

83 string='Storage Path', 

84 readonly=True, 

85 ) 

86 checksum = fields.Char( 

87 string='Checksum', 

88 readonly=True, 

89 ) 

90 

91 # Restore tracking 

92 restore_requested = fields.Boolean( 

93 string='Restore Requested', 

94 default=False, 

95 ) 

96 restore_status = fields.Selection([ 

97 ('none', 'Not Requested'), 

98 ('pending', 'Pending'), 

99 ('in_progress', 'In Progress'), 

100 ('ready', 'Ready for Download'), 

101 ('completed', 'Restored'), 

102 ('failed', 'Failed'), 

103 ], string='Restore Status', default='none') 

104 restore_url = fields.Char( 

105 string='Restore URL', 

106 readonly=True, 

107 ) 

108 restore_expires_at = fields.Datetime( 

109 string='Restore Expires At', 

110 readonly=True, 

111 ) 

112 

113 @api.depends('backup_uuid', 'create_date') 

114 def _compute_display_name(self): 

115 for record in self: 

116 date_str = record.create_date.strftime('%Y-%m-%d %H:%M') if record.create_date else 'New' 

117 uuid_short = record.backup_uuid[:8] if record.backup_uuid else 'pending' 

118 record.display_name = f'{date_str} [{uuid_short}]' 

119 

120 @api.depends('size_bytes') 

121 def _compute_size_display(self): 

122 for record in self: 

123 if not record.size_bytes: 

124 record.size_display = '-' 

125 elif record.size_bytes < 1024: 

126 record.size_display = f'{record.size_bytes:.0f} B' 

127 elif record.size_bytes < 1024 * 1024: 

128 record.size_display = f'{record.size_bytes / 1024:.1f} KB' 

129 elif record.size_bytes < 1024 * 1024 * 1024: 

130 record.size_display = f'{record.size_bytes / (1024 * 1024):.1f} MB' 

131 else: 

132 record.size_display = f'{record.size_bytes / (1024 * 1024 * 1024):.2f} GB' 

133 

134 @api.depends('started_at', 'completed_at') 

135 def _compute_duration(self): 

136 for record in self: 

137 if record.started_at and record.completed_at: 

138 delta = record.completed_at - record.started_at 

139 record.duration = delta.total_seconds() 

140 else: 

141 record.duration = 0 

142 

143 def action_refresh_status(self): 

144 """Refresh backup status from LOX API""" 

145 self.ensure_one() 

146 if not self.backup_uuid: 

147 raise UserError(_('No backup UUID available.')) 

148 

149 api = self.env['lox.api'].create_client(self.config_id) 

150 

151 try: 

152 result = api.get_backup_status(self.backup_uuid) 

153 

154 status_mapping = { 

155 'PENDING': 'pending', 

156 'UPLOADING': 'in_progress', 

157 'VALIDATING': 'validating', 

158 'COMPLETED': 'completed', 

159 'FAILED': 'failed', 

160 'CANCELLED': 'cancelled', 

161 } 

162 

163 updates = {} 

164 if result.get('status'): 

165 updates['status'] = status_mapping.get(result['status'], 'pending') 

166 if result.get('size'): 

167 updates['size_bytes'] = result['size'] 

168 if result.get('checksum'): 

169 updates['checksum'] = result['checksum'] 

170 if result.get('completed_at'): 

171 updates['completed_at'] = result['completed_at'] 

172 if result.get('error'): 

173 updates['error_message'] = result['error'] 

174 

175 if updates: 

176 self.write(updates) 

177 

178 return { 

179 'type': 'ir.actions.client', 

180 'tag': 'display_notification', 

181 'params': { 

182 'title': _('Status Updated'), 

183 'message': _('Backup status: %s') % self.status, 

184 'type': 'info', 

185 'sticky': False, 

186 } 

187 } 

188 except Exception as e: 

189 raise UserError(_('Failed to refresh status: %s') % str(e)) 

190 

191 def action_request_restore(self): 

192 """Request restore/download of this backup""" 

193 self.ensure_one() 

194 if not self.backup_uuid: 

195 raise UserError(_('No backup UUID available.')) 

196 

197 if self.status != 'completed': 

198 raise UserError(_('Only completed backups can be restored.')) 

199 

200 api = self.env['lox.api'].create_client(self.config_id) 

201 

202 try: 

203 result = api.request_restore(self.backup_uuid) 

204 

205 self.write({ 

206 'restore_requested': True, 

207 'restore_status': 'pending', 

208 }) 

209 

210 return { 

211 'type': 'ir.actions.client', 

212 'tag': 'display_notification', 

213 'params': { 

214 'title': _('Restore Requested'), 

215 'message': _('Restore request submitted. Check back soon for download link.'), 

216 'type': 'success', 

217 'sticky': False, 

218 } 

219 } 

220 except Exception as e: 

221 raise UserError(_('Failed to request restore: %s') % str(e)) 

222 

223 def action_get_download_url(self): 

224 """Get download URL for restored backup""" 

225 self.ensure_one() 

226 if not self.backup_uuid: 

227 raise UserError(_('No backup UUID available.')) 

228 

229 api = self.env['lox.api'].create_client(self.config_id) 

230 

231 try: 

232 result = api.get_download_url(self.backup_uuid) 

233 

234 if result.get('url'): 

235 self.write({ 

236 'restore_url': result['url'], 

237 'restore_status': 'ready', 

238 'restore_expires_at': result.get('expires_at'), 

239 }) 

240 

241 return { 

242 'type': 'ir.actions.act_url', 

243 'url': result['url'], 

244 'target': 'new', 

245 } 

246 elif result.get('status') == 'pending': 

247 return { 

248 'type': 'ir.actions.client', 

249 'tag': 'display_notification', 

250 'params': { 

251 'title': _('Not Ready'), 

252 'message': _('Backup is still being prepared for download. Please try again later.'), 

253 'type': 'warning', 

254 'sticky': False, 

255 } 

256 } 

257 else: 

258 raise UserError(_('Download not available: %s') % result.get('error', 'Unknown error')) 

259 except UserError: 

260 raise 

261 except Exception as e: 

262 raise UserError(_('Failed to get download URL: %s') % str(e)) 

263 

264 def action_cancel_backup(self): 

265 """Cancel a pending or in-progress backup""" 

266 self.ensure_one() 

267 if self.status not in ('pending', 'in_progress', 'validating'): 

268 raise UserError(_('Only pending or in-progress backups can be cancelled.')) 

269 

270 # TODO: Call API to cancel backup 

271 self.write({ 

272 'status': 'cancelled', 

273 }) 

274 

275 return { 

276 'type': 'ir.actions.client', 

277 'tag': 'display_notification', 

278 'params': { 

279 'title': _('Backup Cancelled'), 

280 'message': _('Backup has been cancelled.'), 

281 'type': 'info', 

282 'sticky': False, 

283 } 

284 } 

285 

286 @api.model 

287 def cron_refresh_pending_backups(self): 

288 """Cron job to refresh status of pending backups""" 

289 pending_logs = self.search([ 

290 ('status', 'in', ('pending', 'in_progress', 'validating')), 

291 ('backup_uuid', '!=', False), 

292 ]) 

293 

294 for log in pending_logs: 

295 try: 

296 log.action_refresh_status() 

297 except Exception as e: 

298 _logger.error(f'Failed to refresh backup {log.backup_uuid}: {e}')