@@ -5,12 +5,244 @@ function __construct()
55 {
66 parent ::__construct ();
77 }
8+
9+ /* ===== User-level JSON+ZIP backup (Stations, Logbooks, QSOs) ===== */
10+ public function user_export () {
11+ $ this ->load ->model ('user_model ' );
12+ if ($ this ->user_model ->validate_session () == 0 ) { redirect ('user/login ' ); }
13+ if (!$ this ->user_model ->authorize (2 )) { $ this ->session ->set_flashdata ('notice ' , 'You \'re not allowed to do that! ' ); redirect ('dashboard ' ); }
14+
15+ ini_set ('memory_limit ' , '512M ' );
16+ $ this ->load ->model ('Stations ' );
17+ $ this ->load ->model ('Logbook_model ' );
18+ $ this ->load ->dbutil ();
19+
20+ $ tmp_json = tempnam (sys_get_temp_dir (), 'cloudlog_backup_ ' ) . '.json ' ;
21+ $ tmp_zip = tempnam (sys_get_temp_dir (), 'cloudlog_backup_ ' ) . '.zip ' ;
22+
23+ try {
24+ $ user_id = $ this ->session ->userdata ('user_id ' );
25+ if (!$ user_id ) { show_error ('User not authenticated. ' , 401 ); return ; }
26+
27+ while (ob_get_level ()) { ob_end_clean (); }
28+
29+ $ result = array ();
30+
31+ // Stations for user
32+ $ this ->db ->where ('user_id ' , $ user_id );
33+ $ stations = $ this ->db ->get ('station_profile ' )->result_array ();
34+ $ result ['stations ' ] = $ stations ;
35+
36+ // QSOs per station
37+ $ station_qsos = array ();
38+ foreach ($ stations as $ station ) {
39+ if (!isset ($ station ['station_id ' ])) continue ;
40+ $ station_id = $ station ['station_id ' ];
41+ $ qso_chunk_size = 10000 ; $ qso_offset = 0 ; $ qsos = array ();
42+ do {
43+ $ this ->db ->where ('station_id ' , $ station_id );
44+ $ this ->db ->limit ($ qso_chunk_size , $ qso_offset );
45+ $ chunk = $ this ->db ->get ($ this ->config ->item ('table_name ' ))->result_array ();
46+ $ qsos = array_merge ($ qsos , $ chunk ); $ qso_offset += $ qso_chunk_size ;
47+ } while (count ($ chunk ) === $ qso_chunk_size );
48+ $ station_qsos [$ station_id ] = $ qsos ;
49+ }
50+ $ result ['station_qsos ' ] = $ station_qsos ;
51+
52+ // Logbooks for user (include QSOs snapshot for backwards compatibility)
53+ $ logbooks = array ();
54+ $ this ->db ->where ('user_id ' , $ user_id );
55+ foreach ($ this ->db ->get ('station_logbooks ' )->result_array () as $ logbook ) {
56+ $ station_id = isset ($ logbook ['station_id ' ]) ? $ logbook ['station_id ' ] : null ;
57+ $ qso_chunk_size = 10000 ; $ qso_offset = 0 ; $ qsos = array ();
58+ if ($ station_id ) {
59+ do {
60+ $ this ->db ->where ('station_id ' , $ station_id );
61+ $ this ->db ->limit ($ qso_chunk_size , $ qso_offset );
62+ $ chunk = $ this ->db ->get ($ this ->config ->item ('table_name ' ))->result_array ();
63+ $ qsos = array_merge ($ qsos , $ chunk ); $ qso_offset += $ qso_chunk_size ;
64+ } while (count ($ chunk ) === $ qso_chunk_size );
65+ }
66+ $ logbook ['qsos ' ] = $ qsos ; $ logbooks [] = $ logbook ;
67+ }
68+ $ result ['logbooks ' ] = $ logbooks ;
69+
70+ // Relationships if present
71+ $ tables = $ this ->db ->list_tables ();
72+ if (in_array ('station_logbooks_entity ' , $ tables )) {
73+ $ this ->db ->where ('user_id ' , $ user_id );
74+ $ result ['logbooks_entity ' ] = $ this ->db ->get ('station_logbooks_entity ' )->result_array ();
75+ } else { $ result ['logbooks_entity ' ] = []; }
76+
77+ $ result ['schema_version ' ] = '1.0 ' ;
78+ $ result ['exported_at ' ] = date ('c ' );
79+
80+ file_put_contents ($ tmp_json , json_encode ($ result , JSON_PRETTY_PRINT ));
81+ $ zip = new ZipArchive ();
82+ if ($ zip ->open ($ tmp_zip , ZipArchive::CREATE ) !== TRUE ) { throw new Exception ('Could not create ZIP file ' ); }
83+ $ zip ->addFile ($ tmp_json , 'cloudlog_backup.json ' );
84+ $ zip ->close ();
85+
86+ header ('Content-Type: application/zip ' );
87+ header ('Content-Disposition: attachment; filename="cloudlog_backup_ ' .date ('Ymd_His ' ).'.zip" ' );
88+ header ('Content-Length: ' . filesize ($ tmp_zip ));
89+ header ('Cache-Control: no-store, no-cache ' );
90+ readfile ($ tmp_zip );
91+ @unlink ($ tmp_json ); @unlink ($ tmp_zip ); exit ;
92+ } catch (Exception $ e ) { log_message ('error ' , 'User export error: ' .$ e ->getMessage ()); show_error ('Export failed: ' .$ e ->getMessage (), 500 ); }
93+ }
94+
95+ public function user_import () {
96+ $ this ->load ->model ('user_model ' );
97+ if ($ this ->user_model ->validate_session () == 0 ) { redirect ('user/login ' ); }
98+ if (!$ this ->user_model ->authorize (2 )) { $ this ->session ->set_flashdata ('notice ' , 'You \'re not allowed to do that! ' ); redirect ('dashboard ' ); }
99+
100+ ini_set ('memory_limit ' , '512M ' );
101+ $ this ->load ->model ('Stations ' );
102+ $ this ->load ->model ('Logbook_model ' );
103+ $ this ->load ->library ('session ' );
104+
105+ if (!isset ($ _FILES ['backup_file ' ]) || $ _FILES ['backup_file ' ]['error ' ] !== UPLOAD_ERR_OK ) { $ this ->session ->set_flashdata ('notice ' , 'No file uploaded or upload error. ' ); redirect ('backup ' ); return ; }
106+
107+ $ uploaded_file = $ _FILES ['backup_file ' ]['tmp_name ' ];
108+ $ temp_json = tempnam (sys_get_temp_dir (), 'cloudlog_import_ ' ) . '.json ' ;
109+
110+ $ finfo = finfo_open (FILEINFO_MIME_TYPE ); $ mime_type = finfo_file ($ finfo , $ uploaded_file ); finfo_close ($ finfo );
111+ if ($ mime_type === 'application/zip ' ) {
112+ $ zip = new ZipArchive ();
113+ if ($ zip ->open ($ uploaded_file ) === TRUE ) {
114+ $ json_content = $ zip ->getFromName ('cloudlog_backup.json ' ); $ zip ->close ();
115+ if ($ json_content === false ) { $ this ->session ->set_flashdata ('notice ' , 'Invalid backup file: JSON not found in ZIP. ' ); redirect ('backup ' ); return ; }
116+ file_put_contents ($ temp_json , $ json_content ); unset($ json_content );
117+ } else { $ this ->session ->set_flashdata ('notice ' , 'Could not open ZIP file. ' ); redirect ('backup ' ); return ; }
118+ } else { copy ($ uploaded_file , $ temp_json ); }
119+
120+ $ json = file_get_contents ($ temp_json ); $ data = json_decode ($ json , true ); unset($ json );
121+ if (!$ data || !isset ($ data ['stations ' ]) || !isset ($ data ['logbooks ' ])) { @unlink ($ temp_json ); $ this ->session ->set_flashdata ('notice ' , 'Invalid backup file. ' ); redirect ('backup ' ); return ; }
122+
123+ $ station_qsos = isset ($ data ['station_qsos ' ]) ? $ data ['station_qsos ' ] : array ();
124+ $ import_preview = array (
125+ 'stations ' => array_map (function ($ station ) use ($ station_qsos ) {
126+ $ station_id = isset ($ station ['station_id ' ]) ? $ station ['station_id ' ] : null ;
127+ $ qso_count = ($ station_id && isset ($ station_qsos [$ station_id ])) ? count ($ station_qsos [$ station_id ]) : 0 ;
128+ return array (
129+ 'station_id ' => $ station_id ,
130+ 'station_callsign ' => $ station ['station_callsign ' ],
131+ 'station_profile_name ' => $ station ['station_profile_name ' ],
132+ 'qsos_count ' => $ qso_count ,
133+ );
134+ }, $ data ['stations ' ]),
135+ 'logbooks ' => array_map (function ($ logbook ) {
136+ $ qso_count = isset ($ logbook ['qsos ' ]) ? count ($ logbook ['qsos ' ]) : 0 ;
137+ return array (
138+ 'logbook_id ' => $ logbook ['logbook_id ' ],
139+ 'logbook_name ' => $ logbook ['logbook_name ' ],
140+ 'station_id ' => isset ($ logbook ['station_id ' ]) ? $ logbook ['station_id ' ] : null ,
141+ 'qsos_count ' => $ qso_count ,
142+ );
143+ }, $ data ['logbooks ' ]),
144+ );
145+
146+ $ this ->session ->set_userdata ('import_backup_file ' , $ temp_json );
147+ // HTMX partial
148+ $ this ->load ->view ('backup/import_preview ' , $ import_preview );
149+ }
150+
151+ public function user_do_import () {
152+ $ this ->load ->model ('user_model ' );
153+ if ($ this ->user_model ->validate_session () == 0 ) { redirect ('user/login ' ); }
154+ if (!$ this ->user_model ->authorize (2 )) { echo '<div class="alert alert-danger">Not authorized</div> ' ; return ; }
155+
156+ ini_set ('memory_limit ' , '512M ' );
157+ $ this ->load ->model ('Stations ' );
158+ $ this ->load ->model ('Logbook_model ' );
159+ $ this ->load ->library ('session ' );
160+
161+ $ temp_file = $ this ->session ->userdata ('import_backup_file ' );
162+ if (!$ temp_file || !file_exists ($ temp_file )) { echo '<div class="alert alert-danger">No import data found.</div> ' ; return ; }
163+ $ json = file_get_contents ($ temp_file ); $ data = json_decode ($ json , true ); unset($ json );
164+ if (!$ data ) { echo '<div class="alert alert-danger">Failed to parse backup data.</div> ' ; @unlink ($ temp_file ); $ this ->session ->unset_userdata ('import_backup_file ' ); return ; }
165+
166+ $ import_stations = $ this ->input ->post ('import_stations ' );
167+ $ import_logbooks = $ this ->input ->post ('import_logbooks ' );
168+ if (!is_array ($ import_stations )) $ import_stations = array ();
169+ if (!is_array ($ import_logbooks )) $ import_logbooks = array ();
170+ if (empty ($ import_stations ) && empty ($ import_logbooks )) { echo '<div class="alert alert-warning">Please select at least one station or logbook to import.</div> ' ; return ; }
171+
172+ $ current_user_id = $ this ->session ->userdata ('user_id ' );
173+ if (!$ current_user_id ) { echo '<div class="alert alert-danger">User not authenticated.</div> ' ; return ; }
174+
175+ $ total_stations = count ($ import_stations ); $ total_logbooks = count ($ import_logbooks ); $ total_qsos = 0 ;
176+ foreach ($ data ['logbooks ' ] as $ logbook ) { if (in_array ($ logbook ['logbook_id ' ], $ import_logbooks ) && isset ($ logbook ['qsos ' ])) { $ total_qsos += count ($ logbook ['qsos ' ]); } }
177+ if (isset ($ data ['station_qsos ' ])) { foreach ($ data ['station_qsos ' ] as $ sid => $ qsosArr ) { if (in_array ($ sid , $ import_stations )) { $ total_qsos += count ($ qsosArr ); } } }
178+
179+ $ imported = array ('stations ' =>0 ,'logbooks ' =>0 ,'qsos ' =>0 ,'conflicts ' =>array (),'step ' =>'' ,'total_stations ' =>$ total_stations ,'total_logbooks ' =>$ total_logbooks ,'total_qsos ' =>$ total_qsos );
180+ $ station_id_map = array ();
181+
182+ // Stations
183+ $ imported ['step ' ] = 'Importing stations ' ;
184+ foreach ($ data ['stations ' ] as $ station ) {
185+ if (!in_array ($ station ['station_id ' ], $ import_stations )) continue ; $ old_station_id = $ station ['station_id ' ];
186+ $ this ->db ->where ('station_callsign ' , $ station ['station_callsign ' ]);
187+ $ this ->db ->where ('station_profile_name ' , $ station ['station_profile_name ' ]);
188+ $ this ->db ->where ('user_id ' , $ current_user_id );
189+ $ conflict = $ this ->db ->get ('station_profile ' )->row_array ();
190+ if ($ conflict ) { $ imported ['conflicts ' ][] = 'Station exists (reusing): ' .$ station ['station_callsign ' ].' - ' .$ station ['station_profile_name ' ].' (ID ' .$ conflict ['station_id ' ].') ' ; $ station_id_map [$ old_station_id ] = $ conflict ['station_id ' ]; continue ; }
191+ unset($ station ['station_id ' ]); $ station ['user_id ' ] = $ current_user_id ; $ station ['station_active ' ] = 0 ; // never active on import
192+ $ this ->db ->insert ('station_profile ' , $ station ); $ new_station_id = $ this ->db ->insert_id (); if (!$ new_station_id ) { $ imported ['conflicts ' ][] = 'Failed to create station: ' .$ station ['station_callsign ' ].' - ' .$ station ['station_profile_name ' ]; continue ; }
193+ $ station_id_map [$ old_station_id ] = $ new_station_id ; $ imported ['stations ' ]++;
194+ }
195+
196+ // Logbooks
197+ $ imported ['step ' ] = 'Importing logbooks ' ;
198+ foreach ($ data ['logbooks ' ] as $ logbook ) {
199+ if (!in_array ($ logbook ['logbook_id ' ], $ import_logbooks )) continue ;
200+ $ this ->db ->where ('logbook_name ' , $ logbook ['logbook_name ' ]); $ this ->db ->where ('user_id ' , $ current_user_id ); $ conflict = $ this ->db ->get ('station_logbooks ' )->row_array ();
201+ if ($ conflict ) { $ imported ['conflicts ' ][] = 'Logbook exists (skipping): ' .$ logbook ['logbook_name ' ]; continue ; }
202+ $ logbook_copy = $ logbook ; unset($ logbook_copy ['qsos ' ]); unset($ logbook_copy ['logbook_id ' ]); if (isset ($ logbook_copy ['active_logbook ' ])) unset($ logbook_copy ['active_logbook ' ]); $ logbook_copy ['user_id ' ] = $ current_user_id ; if (isset ($ logbook_copy ['station_id ' ]) && isset ($ station_id_map [$ logbook_copy ['station_id ' ]])) { $ logbook_copy ['station_id ' ] = $ station_id_map [$ logbook_copy ['station_id ' ]]; }
203+ $ this ->db ->insert ('station_logbooks ' , $ logbook_copy ); $ imported ['logbooks ' ]++;
204+ }
205+
206+ // QSOs from station_qsos
207+ $ imported ['step ' ] = 'Importing QSOs ' ;
208+ if (isset ($ data ['station_qsos ' ])) {
209+ foreach ($ data ['station_qsos ' ] as $ old_station_id => $ qsos ) {
210+ if (!in_array ($ old_station_id , $ import_stations )) continue ; if (!isset ($ station_id_map [$ old_station_id ])) continue ; $ new_station_id = $ station_id_map [$ old_station_id ];
211+ foreach (array_chunk ($ qsos , 1000 ) as $ qso_chunk ) {
212+ foreach ($ qso_chunk as $ qso ) {
213+ unset($ qso ['COL_PRIMARY_KEY ' ]); $ qso ['station_id ' ] = $ new_station_id ;
214+ $ this ->db ->where ('COL_TIME_ON ' , $ qso ['COL_TIME_ON ' ]); $ this ->db ->where ('COL_CALL ' , $ qso ['COL_CALL ' ]); $ this ->db ->where ('station_id ' , $ qso ['station_id ' ]);
215+ $ conflict = $ this ->db ->get ($ this ->config ->item ('table_name ' ))->row_array (); if ($ conflict ) { $ imported ['conflicts ' ][] = 'QSO conflict: ' .$ qso ['COL_CALL ' ].' @ ' .$ qso ['COL_TIME_ON ' ]; continue ; }
216+ $ this ->db ->insert ($ this ->config ->item ('table_name ' ), $ qso ); $ imported ['qsos ' ]++;
217+ }
218+ }
219+ }
220+ }
221+ // QSOs from logbooks (legacy)
222+ foreach ($ data ['logbooks ' ] as $ logbook ) {
223+ if (!in_array ($ logbook ['logbook_id ' ], $ import_logbooks )) continue ; if (!isset ($ logbook ['qsos ' ])) continue ;
224+ foreach (array_chunk ($ logbook ['qsos ' ], 1000 ) as $ qso_chunk ) {
225+ foreach ($ qso_chunk as $ qso ) {
226+ unset($ qso ['COL_PRIMARY_KEY ' ]); if (isset ($ qso ['station_id ' ]) && isset ($ station_id_map [$ qso ['station_id ' ]])) { $ qso ['station_id ' ] = $ station_id_map [$ qso ['station_id ' ]]; }
227+ $ this ->db ->where ('COL_TIME_ON ' , $ qso ['COL_TIME_ON ' ]); $ this ->db ->where ('COL_CALL ' , $ qso ['COL_CALL ' ]); $ this ->db ->where ('station_id ' , $ qso ['station_id ' ]);
228+ $ conflict = $ this ->db ->get ($ this ->config ->item ('table_name ' ))->row_array (); if ($ conflict ) { $ imported ['conflicts ' ][] = 'QSO conflict: ' .$ qso ['COL_CALL ' ].' @ ' .$ qso ['COL_TIME_ON ' ]; continue ; }
229+ $ this ->db ->insert ($ this ->config ->item ('table_name ' ), $ qso ); $ imported ['qsos ' ]++;
230+ }
231+ }
232+ }
233+
234+ $ temp_file = $ this ->session ->userdata ('import_backup_file ' ); if ($ temp_file && file_exists ($ temp_file )) { @unlink ($ temp_file ); }
235+ $ this ->session ->unset_userdata ('import_backup_file ' );
236+
237+ $ this ->load ->view ('backup/import_progress ' , $ imported );
238+ }
8239
9240 /* User Facing Links to Backup URLs */
10241 public function index ()
11242 {
12243 $ this ->load ->model ('user_model ' );
13- if (!$ this ->user_model ->authorize (99 )) { $ this ->session ->set_flashdata ('notice ' , 'You \'re not allowed to do that! ' ); redirect ('dashboard ' ); }
244+ // Allow all authenticated operators (level 2+) instead of only admins
245+ if (!$ this ->user_model ->authorize (2 )) { $ this ->session ->set_flashdata ('notice ' , 'You \'re not allowed to do that! ' ); redirect ('dashboard ' ); }
14246
15247 $ data ['page_title ' ] = "Backup " ;
16248
0 commit comments