Fortran · 11526 bytes Raw Blame History
1 ! Backup system module
2 ! Handles auto-backup of modified buffers and restoration
3
4 module backup_module
5 use iso_fortran_env, only: int32, int64
6 implicit none
7 private
8
9 public :: backup_create, backup_detect, backup_list, backup_restore
10 public :: backup_delete, backup_prompt_restore, backup_info_t
11
12 integer, parameter :: MAX_PATH_LEN = 512
13 integer, parameter :: MAX_BACKUPS = 100
14
15 type :: backup_info_t
16 character(len=MAX_PATH_LEN) :: original_file = ""
17 character(len=MAX_PATH_LEN) :: backup_file = ""
18 character(len=MAX_PATH_LEN) :: timestamp = ""
19 integer(int64) :: file_size = 0
20 end type backup_info_t
21
22 contains
23
24 !> Create backup of a file
25 subroutine backup_create(workspace_path, file_path, success)
26 character(len=*), intent(in) :: workspace_path, file_path
27 logical, intent(out) :: success
28 character(len=MAX_PATH_LEN) :: backup_dir, backup_file, basename
29 character(len=MAX_PATH_LEN) :: src_file, timestamp_str
30 integer :: unit_src, unit_dst, ios, i
31 character(len=1024) :: line
32
33 success = .false.
34
35 ! Create backup directory if needed
36 backup_dir = trim(workspace_path) // "/.fac/backups"
37 call execute_command_line("mkdir -p '" // trim(backup_dir) // "'", wait=.true.)
38
39 ! Extract basename from file_path
40 basename = file_path
41 do i = len_trim(file_path), 1, -1
42 if (file_path(i:i) == '/') then
43 basename = file_path(i+1:)
44 exit
45 end if
46 end do
47
48 ! Generate timestamp-based backup filename
49 call get_timestamp(timestamp_str)
50 write(backup_file, '(A,A,A,A,A)') trim(backup_dir), '/', trim(basename), '.', trim(timestamp_str)
51
52 ! Copy file to backup
53 src_file = file_path
54 open(newunit=unit_src, file=src_file, status='old', action='read', iostat=ios)
55 if (ios /= 0) return
56
57 open(newunit=unit_dst, file=backup_file, status='replace', action='write', iostat=ios)
58 if (ios /= 0) then
59 close(unit_src)
60 return
61 end if
62
63 ! Copy line by line
64 do
65 read(unit_src, '(A)', iostat=ios) line
66 if (ios /= 0) exit
67 write(unit_dst, '(A)') trim(line)
68 end do
69
70 close(unit_src)
71 close(unit_dst)
72
73 ! Update metadata
74 call backup_update_metadata(workspace_path, file_path, backup_file, timestamp_str)
75
76 success = .true.
77 end subroutine backup_create
78
79 !> Detect if backups exist for workspace
80 function backup_detect(workspace_path) result(has_backups)
81 character(len=*), intent(in) :: workspace_path
82 logical :: has_backups
83 character(len=MAX_PATH_LEN) :: metadata_file
84 integer :: unit, ios
85
86 has_backups = .false.
87 metadata_file = trim(workspace_path) // "/.fac/backups/.backup-metadata.json"
88
89 open(newunit=unit, file=metadata_file, status='old', iostat=ios)
90 if (ios == 0) then
91 close(unit)
92 has_backups = .true.
93 end if
94 end function backup_detect
95
96 !> List all backups for workspace
97 subroutine backup_list(workspace_path, backups, count)
98 character(len=*), intent(in) :: workspace_path
99 type(backup_info_t), allocatable, intent(out) :: backups(:)
100 integer, intent(out) :: count
101 character(len=MAX_PATH_LEN) :: metadata_file, line
102 integer :: unit, ios
103
104 count = 0
105 allocate(backups(MAX_BACKUPS))
106
107 metadata_file = trim(workspace_path) // "/.fac/backups/.backup-metadata.json"
108
109 open(newunit=unit, file=metadata_file, status='old', iostat=ios)
110 if (ios /= 0) return
111
112 ! Simple JSON parsing (assumes one backup per "original_file" line)
113 do
114 read(unit, '(A)', iostat=ios) line
115 if (ios /= 0) exit
116
117 if (index(line, '"original_file":') > 0) then
118 count = count + 1
119 if (count > MAX_BACKUPS) exit
120 call parse_backup_entry(line, backups(count))
121 end if
122 end do
123
124 close(unit)
125 end subroutine backup_list
126
127 !> Restore a backup file
128 subroutine backup_restore(backup_file, original_file, success)
129 character(len=*), intent(in) :: backup_file, original_file
130 logical, intent(out) :: success
131 integer :: unit_src, unit_dst, ios
132 character(len=1024) :: line
133
134 success = .false.
135
136 ! Copy backup to original location using native Fortran I/O
137 ! (avoids execute_command_line issues with flang)
138 open(newunit=unit_src, file=trim(backup_file), status='old', action='read', iostat=ios)
139 if (ios /= 0) return
140
141 open(newunit=unit_dst, file=trim(original_file), status='replace', action='write', iostat=ios)
142 if (ios /= 0) then
143 close(unit_src)
144 return
145 end if
146
147 ! Copy line by line
148 do
149 read(unit_src, '(A)', iostat=ios) line
150 if (ios /= 0) exit
151 write(unit_dst, '(A)') trim(line)
152 end do
153
154 close(unit_src)
155 close(unit_dst)
156
157 ! Delete the backup after successful restore
158 call backup_delete(backup_file)
159 success = .true.
160 end subroutine backup_restore
161
162 !> Delete a backup file
163 subroutine backup_delete(backup_file)
164 character(len=*), intent(in) :: backup_file
165 integer :: unit, ios
166
167 ! Delete using Fortran file operations (avoids execute_command_line issues)
168 open(newunit=unit, file=trim(backup_file), status='old', iostat=ios)
169 if (ios == 0) then
170 close(unit, status='delete')
171 end if
172 end subroutine backup_delete
173
174 !> Prompt user to restore backup (returns 'r', 'd' (delete), or 'c' (compare))
175 function backup_prompt_restore(original_file, current_backup, total_backups, backup_timestamp) result(choice)
176 use terminal_io_module, only: terminal_write, terminal_move_cursor, terminal_clear_screen
177 use input_handler_module, only: get_key_input
178 character(len=*), intent(in) :: original_file
179 integer, intent(in), optional :: current_backup, total_backups
180 character(len=*), intent(in), optional :: backup_timestamp
181 character :: choice
182 character(len=32) :: key_input
183 character(len=512) :: prompt
184 integer :: status
185
186 ! Clear screen and show prompt
187 call terminal_clear_screen()
188 call terminal_move_cursor(2, 1)
189
190 ! Build prompt with progress and timestamp if available
191 if (present(current_backup) .and. present(total_backups)) then
192 if (present(backup_timestamp)) then
193 write(prompt, '(A,I0,A,I0,A,A,A,A,A)') 'Backup found [', current_backup, &
194 ' of ', total_backups, ']: ', trim(original_file), ' (from ', trim(backup_timestamp), ')'
195 else
196 write(prompt, '(A,I0,A,I0,A,A)') 'Backup found [', current_backup, &
197 ' of ', total_backups, ']: ', trim(original_file)
198 end if
199 else
200 if (present(backup_timestamp)) then
201 write(prompt, '(A,A,A,A,A)') 'Backup found for: ', trim(original_file), &
202 ' (from ', trim(backup_timestamp), ')'
203 else
204 write(prompt, '(A,A)') 'Backup found for: ', trim(original_file)
205 end if
206 end if
207 call terminal_write(trim(prompt))
208
209 call terminal_move_cursor(4, 1)
210 call terminal_write('[r]estore - Use backup (current file will be replaced)')
211
212 call terminal_move_cursor(5, 1)
213 call terminal_write('[d]elete - Delete backup and keep current file')
214
215 call terminal_move_cursor(6, 1)
216 call terminal_write('[c]ompare - Show differences between files')
217
218 call terminal_move_cursor(8, 1)
219 call terminal_write('Choice: ')
220
221 ! Get user input
222 do
223 call get_key_input(key_input, status)
224 if (status == 0) then
225 if (key_input == 'r' .or. key_input == 'R') then
226 choice = 'r'
227 exit
228 else if (key_input == 'd' .or. key_input == 'D') then
229 choice = 'd' ! Delete backup
230 exit
231 else if (key_input == 'c' .or. key_input == 'C') then
232 choice = 'c' ! Compare
233 exit
234 else if (key_input == 'k' .or. key_input == 'K' .or. key_input == 'i' .or. key_input == 'I') then
235 ! Accept 'k' and 'i' for backwards compatibility
236 choice = 'd' ! Map to delete
237 exit
238 else if (key_input == 'ESCAPE') then
239 choice = 'd' ! ESC = delete backup
240 exit
241 end if
242 end if
243 end do
244 end function backup_prompt_restore
245
246 !> Get current timestamp string (Unix epoch)
247 subroutine get_timestamp(timestamp_str)
248 character(len=*), intent(out) :: timestamp_str
249 integer :: values(8)
250 integer(int64) :: epoch
251
252 ! Get current time
253 call date_and_time(values=values)
254
255 ! Simple epoch calculation (approximate)
256 epoch = int(values(1) - 1970, int64) * 365_int64 * 86400_int64 + &
257 int(values(2), int64) * 30_int64 * 86400_int64 + &
258 int(values(3), int64) * 86400_int64 + &
259 int(values(5), int64) * 3600_int64 + &
260 int(values(6), int64) * 60_int64 + &
261 int(values(7), int64)
262
263 write(timestamp_str, '(I0)') epoch
264 end subroutine get_timestamp
265
266 !> Update backup metadata JSON
267 subroutine backup_update_metadata(workspace_path, original_file, backup_file, timestamp)
268 character(len=*), intent(in) :: workspace_path, original_file, backup_file, timestamp
269 character(len=MAX_PATH_LEN) :: metadata_file
270 integer :: unit, ios
271
272 metadata_file = trim(workspace_path) // "/.fac/backups/.backup-metadata.json"
273
274 ! Simple append for now (not parsing existing JSON)
275 open(newunit=unit, file=metadata_file, status='old', position='append', iostat=ios)
276 if (ios /= 0) then
277 ! Create new file
278 open(newunit=unit, file=metadata_file, status='replace')
279 write(unit, '(A)') '{'
280 write(unit, '(A)') ' "backups": ['
281 end if
282
283 ! Write backup entry (simplified JSON)
284 write(unit, '(A)') ' {'
285 write(unit, '(A)') ' "original_file": "' // trim(original_file) // '",'
286 write(unit, '(A)') ' "backup_file": "' // trim(backup_file) // '",'
287 write(unit, '(A)') ' "timestamp": "' // trim(timestamp) // '"'
288 write(unit, '(A)') ' }'
289
290 close(unit)
291 end subroutine backup_update_metadata
292
293 !> Parse a backup entry from JSON line (simplified)
294 subroutine parse_backup_entry(line, backup)
295 character(len=*), intent(in) :: line
296 type(backup_info_t), intent(out) :: backup
297 integer :: quote1, quote2
298
299 ! Extract original_file (very simple parsing)
300 quote1 = index(line, '"', .true.)
301 if (quote1 > 0) then
302 quote2 = index(line(1:quote1-1), '"', .true.)
303 if (quote2 > 0) then
304 backup%original_file = line(quote2+1:quote1-1)
305 end if
306 end if
307 end subroutine parse_backup_entry
308
309 end module backup_module
310