Version 0.1.7.
[guestfs-browser.git] / filetree_ops.ml
1 (* Guestfs Browser.
2  * Copyright (C) 2010 Red Hat Inc.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17  *)
18
19 open Printf
20
21 open Utils
22 open Slave_types
23
24 open Filetree_type
25 open Filetree_markup
26
27 (* Temporary directory for shared use by any function in this file.
28  * It is cleaned up when the program exits.
29  *)
30 let tmpdir = tmpdir ()
31
32 (* Get the basename of a file, using path conventions which are valid
33  * for libguestfs.  So [Filename.basename] won't necessarily work
34  * because it will use host path conventions.
35  *)
36 let basename pathname =
37   let len = String.length pathname in
38   try
39     let i = String.rindex pathname '/' in
40     let r = String.sub pathname (i+1) (len-i-1) in
41     if r = "" then "root" else r
42   with
43     Not_found -> pathname
44
45 (* Get the extension of a file using libguestfs path conventions,
46  * including the leading point (eg. ".txt").  Might return an empty
47  * string if there is no extension.
48  *)
49 let extension pathname =
50   let len = String.length pathname in
51   try
52     let i = String.rindex pathname '.' in
53     let r = String.sub pathname i (len-i) in
54     r
55   with
56     Not_found -> ""
57
58 (* Download a single file. *)
59 let rec download_file ({ model = model } as t) path () =
60   let row = model#get_iter path in
61   let src, pathname = get_pathname t row in
62   debug "download_file %s: showing dialog" pathname;
63
64   (* Put up the dialog. *)
65   let title = "Download file" in
66   let dlg = GWindow.file_chooser_dialog ~action:`SAVE ~title ~modal:true () in
67   dlg#add_button_stock `CANCEL `CANCEL;
68   dlg#add_select_button_stock `SAVE `SAVE;
69   dlg#set_current_name (basename pathname);
70
71   match dlg#run () with
72   | `DELETE_EVENT | `CANCEL ->
73       dlg#destroy ()
74   | `SAVE ->
75       match dlg#filename with
76       | None -> ()
77       | Some localfile ->
78           dlg#destroy ();
79
80           (* Download the file. *)
81           Slave.download_file src pathname localfile
82             (when_downloaded_file t path)
83
84 and when_downloaded_file ({ model = model } as t) path () =
85   let row = model#get_iter path in
86   set_visited t row
87
88 (* Download a directory as a tarball. *)
89 let rec download_dir_tarball ({ model = model } as t) format path () =
90   let row = model#get_iter path in
91   let src, pathname = get_pathname t row in
92   debug "download_dir_tarball %s: showing dialog" pathname;
93
94   (* Put up the dialog. *)
95   let title = "Download directory to tar file" in
96   let dlg = GWindow.file_chooser_dialog ~action:`SAVE ~title ~modal:true () in
97   dlg#add_button_stock `CANCEL `CANCEL;
98   dlg#add_select_button_stock `SAVE `SAVE;
99
100   let extension = match format with
101     | Tar -> ".tar"
102     | TGZ -> ".tar.gz"
103     | TXZ -> ".tar.xz"
104   in
105   dlg#set_current_name (basename pathname ^ extension);
106
107   match dlg#run () with
108   | `DELETE_EVENT | `CANCEL ->
109       dlg#destroy ()
110   | `SAVE ->
111       match dlg#filename with
112       | None -> ()
113       | Some localfile ->
114           dlg#destroy ();
115
116           (* Download the directory. *)
117           Slave.download_dir_tarball src pathname format localfile
118             (when_downloaded_dir_tarball t path)
119
120 and when_downloaded_dir_tarball ({ model = model } as t) path () =
121   let row = model#get_iter path in
122   set_visited t row
123
124 let rec download_dir_find0 ({ model = model } as t) path () =
125   let row = model#get_iter path in
126   let src, pathname = get_pathname t row in
127   debug "download_dir_find0 %s: showing dialog" pathname;
128
129   (* Put up the dialog. *)
130   let title = "Download list of filenames" in
131   let dlg = GWindow.file_chooser_dialog ~action:`SAVE ~title ~modal:true () in
132   dlg#add_button_stock `CANCEL `CANCEL;
133   dlg#add_select_button_stock `SAVE `SAVE;
134   dlg#set_current_name (basename pathname ^ ".filenames.txt");
135
136   (* Notify that the list of strings is \0 separated. *)
137   let hbox =
138     let hbox = GPack.hbox () in
139     ignore (GMisc.image ~stock:`INFO ~packing:hbox#pack ());
140     let label = GMisc.label
141       ~text:"The list of filenames is saved to a file with zero byte separators, to allow the full range of characters to be used in the names themselves."
142       ~packing:hbox#pack () in
143     label#set_line_wrap true;
144     hbox in
145   dlg#set_extra_widget (hbox :> GObj.widget);
146
147   match dlg#run () with
148   | `DELETE_EVENT | `CANCEL ->
149       dlg#destroy ()
150   | `SAVE ->
151       match dlg#filename with
152       | None -> ()
153       | Some localfile ->
154           dlg#destroy ();
155
156           (* Download the directory. *)
157           Slave.download_dir_find0 src pathname localfile
158             (when_downloaded_dir_find0 t path)
159
160 and when_downloaded_dir_find0 ({ model = model } as t) path () =
161   let row = model#get_iter path in
162   set_visited t row
163
164 let has_child_node_equals t row content =
165   try ignore (find_child_node_by_content t row content); true
166   with Not_found -> false
167
168 (* Calculate disk space used by a directory. *)
169 let rec disk_usage ({ model = model } as t) path () =
170   t.view#expand_row path;
171
172   let row = model#get_iter path in
173   let src, pathname = get_pathname t row in
174   debug "disk_usage %s" pathname;
175
176   (* See if this node already has an Info "disk_usage" child node.  If
177    * so they don't recreate it.
178    *)
179   let content = Info "disk_usage" in
180   if not (has_child_node_equals t row content) then (
181     (* Create the child node first. *)
182     let row = model#insert ~parent:row 0 in
183     let hdata = { state=IsLeaf; content=content; visited=false; hiveh=None } in
184     store_hdata t row hdata;
185     model#set ~row ~column:t.name_col "<i>Calculating disk usage ...</i>";
186
187     Slave.disk_usage src pathname (when_disk_usage t path pathname)
188   )
189
190 and when_disk_usage ({ model = model } as t) path pathname kbytes =
191   let row = model#get_iter path in
192
193   (* Find the Info "disk_usage" child node added above, and replace the
194    * text in it with the final size.
195    *)
196   try
197     let content = Info "disk_usage" in
198     let row = find_child_node_by_content t row content in
199     let msg =
200       sprintf "<b>%s</b>\n<small>Disk usage of %s (%Ld KB)</small>"
201         (human_size_1k kbytes) pathname kbytes in
202     model#set ~row ~column:t.name_col msg
203   with
204     Not_found -> ()
205
206 (* Display operating system inspection information. *)
207 let display_inspection_data ({ model = model } as t) path () =
208   t.view#expand_row path;
209
210   let row = model#get_iter path in
211   let src, _ = get_pathname t row in
212   debug "display_inspection_data";
213
214   (* Should be an OS source, if not ignore. *)
215   match src with
216   | Volume _ -> ()
217   | OS os ->
218       (* See if this node already has an Info "inspection_data" child
219        * node.  If so they don't recreate it.
220        *)
221       let content = Info "inspection_data" in
222       if not (has_child_node_equals t row content) then (
223         let row = model#insert ~parent:row 0 in
224         let hdata =
225           { state=IsLeaf; content=content; visited=false; hiveh=None } in
226         store_hdata t row hdata;
227
228         (* XXX UGHLEE *)
229         let data =
230           sprintf "Type: <b>%s</b>\nDistro: <b>%s</b>\nVersion: <b>%d.%d</b>\nArch.: <b>%s</b>\nPackaging: <b>%s</b>/<b>%s</b>\n%sMountpoints:\n%s"
231             os.insp_type os.insp_distro
232             os.insp_major_version os.insp_minor_version
233             os.insp_arch
234             os.insp_package_management os.insp_package_format
235             (match os.insp_windows_systemroot with
236              | None -> ""
237              | Some path ->
238                  sprintf "Systemroot: <b>%s</b>\n" (markup_escape path))
239             (String.concat "\n"
240                (List.map (
241                   fun (mp, dev) ->
242                     sprintf "<b>%s</b> on <b>%s</b>"
243                       (markup_escape dev) (markup_escape mp))
244                   os.insp_mountpoints)
245             ) in
246
247         model#set ~row ~column:t.name_col data
248       )
249
250 (* Copy registry key value to clipboard. *)
251 let copy_regvalue ({ model = model } as t) path () =
252   let row = model#get_iter path in
253   let hdata = get_hdata t row in
254   match hdata with
255   | { content=RegValue value; hiveh = Some h } ->
256       let t, v = Hivex.value_value h value in
257       let v = printable_hivex_value t v in
258       let cb = GData.clipboard Gdk.Atom.clipboard in
259       cb#set_text v
260
261   | _ -> () (* not a registry value row, ignore *)
262
263 (* View a single file. *)
264 let rec view_file ({ model = model } as t) path opener () =
265   let row = model#get_iter path in
266   let src, pathname = get_pathname t row in
267   debug "view_file %s" pathname;
268
269   (* Download the file into a temporary directory. *)
270   let ext = extension pathname in
271   let localfile = tmpdir // string_of_int (unique ()) ^ ext in
272   Slave.download_file src pathname localfile
273     (when_downloaded_file_for_view t path opener localfile)
274
275 and when_downloaded_file_for_view ({ model = model } as t) path
276     opener localfile () =
277   let row = model#get_iter path in
278   set_visited t row;
279
280   let cmd =
281     sprintf "%s %s" (Filename.quote opener) (Filename.quote localfile) in
282   Slave.run_command cmd Slave.no_callback
283
284 (* Compute the checksum of a file. *)
285 let rec checksum_file ({ model = model } as t) path csumtype () =
286   let row = model#get_iter path in
287   let src, pathname = get_pathname t row in
288   debug "checksum_file %s" pathname;
289
290   (* See if this node already has an Info "checksum" child
291    * node.  If so they don't recreate it.
292    *)
293   let content = Info ("checksum:" ^ csumtype) in
294   if not (has_child_node_equals t row content) then (
295     let row = model#insert ~parent:row 0 in
296     let hdata =
297       { state=IsLeaf; content=content; visited=false; hiveh=None } in
298     store_hdata t row hdata;
299     model#set ~row ~column:t.name_col
300       (sprintf "<i>Calculating %s ...</i>" csumtype);
301
302     t.view#expand_row path;
303
304     Slave.checksum_file src pathname csumtype
305       (when_checksum_file t path pathname csumtype)
306   )
307
308 and when_checksum_file ({ model = model } as t) path pathname csumtype checksum=
309   let row = model#get_iter path in
310   set_visited t row;
311
312   (* Find the child node added above, and replace the text. *)
313   try
314     let content = Info ("checksum:" ^ csumtype) in
315     let row = find_child_node_by_content t row content in
316     let msg = sprintf "%s: %s" csumtype checksum in
317     model#set ~row ~column:t.name_col msg
318   with
319     Not_found -> ()
320
321 (* Compute the file information of a file. *)
322 let rec file_information ({ model = model } as t) path () =
323   let row = model#get_iter path in
324   let src, pathname = get_pathname t row in
325   debug "file_information %s" pathname;
326
327   (* See if this node already has an Info "file_information" child
328    * node.  If so they don't recreate it.
329    *)
330   let content = Info "file_information" in
331   if not (has_child_node_equals t row content) then (
332     let row = model#insert ~parent:row 0 in
333     let hdata =
334       { state=IsLeaf; content=content; visited=false; hiveh=None } in
335     store_hdata t row hdata;
336     model#set ~row ~column:t.name_col "<i>Calculating file information ...</i>";
337
338     t.view#expand_row path;
339
340     Slave.file_information src pathname (when_file_information t path pathname)
341   )
342
343 and when_file_information ({ model = model } as t) path pathname info =
344   let row = model#get_iter path in
345   set_visited t row;
346
347   (* Find the child node added above, and replace the text. *)
348   try
349     let content = Info "file_information" in
350     let row = find_child_node_by_content t row content in
351     model#set ~row ~column:t.name_col (markup_escape info)
352   with
353     Not_found -> ()
354
355 (* Export a registry key/subkey tree as a reg file.  This is pretty
356  * effortless with hivexregedit.
357  *)
358 let download_as_reg ({ model = model } as t) path hivexregedit () =
359   let row = model#get_iter path in
360   let hdata = get_hdata t row in
361       
362   (* Get path to the top of the registry tree. *)
363   let (_, rootkey, _, cachefile), nodes = get_registry_path t row in
364   let regpath = String.concat "\\" (List.rev nodes) in
365   debug "download_as_reg: %s %s %s" cachefile rootkey regpath;
366
367   let do_dialog () =
368     (* Put up the dialog. *)
369     let title = "Download as .reg file" in
370     let dlg = GWindow.file_chooser_dialog
371       ~action:`SAVE ~title ~modal:true () in
372     dlg#add_button_stock `CANCEL `CANCEL;
373     dlg#add_select_button_stock `SAVE `SAVE;
374     let name = match nodes with [] -> rootkey | (name::_) -> name in
375     dlg#set_current_name (name ^ ".reg");
376
377     match dlg#run () with
378     | `DELETE_EVENT | `CANCEL ->
379         dlg#destroy ()
380     | `SAVE ->
381         match dlg#filename with
382         | None -> ()
383         | Some localfile ->
384             dlg#destroy ();
385
386             (* Use hivexregedit to save it. *)
387             let cmd =
388               sprintf "%s --export --prefix %s %s %s > %s"
389                 (Filename.quote hivexregedit)
390                 (Filename.quote rootkey) (Filename.quote cachefile)
391                 (Filename.quote regpath) (Filename.quote localfile) in
392             Slave.run_command cmd Slave.no_callback
393   in
394
395   match hdata with
396   | { content=RegKey _ } ->
397       do_dialog ()
398
399   | { content=TopWinReg (src, _, remotefile, cachefile) } ->
400       (* There's a subtle problem here: If the top node has not been
401        * opened, the registry cachefile won't have been downloaded.  If
402        * the top node has been opened, the registry might still be
403        * being downloaded as we are running here.  Either way we can't
404        * trust the cachefile.  Tell the slave thread to download the
405        * file if it's not downloaded already (since the slave thread
406        * runs in a serial loop, this is always race free).
407        *)
408       cache_registry_file t path src remotefile cachefile do_dialog
409
410   | _ -> () (* not a registry key, ignore *)