1 /*
2 ** 2000-02-12 - A Join command, to counter the long-existing (um, but still incomplete)
3 ** Split command. Features neat drag-and-drop support for ordering the files
4 ** to be joined.
5 */
6
7 #include "gentoo.h"
8
9 #include <ctype.h>
10 #include <fcntl.h>
11
12 #include "cmd_copy.h"
13 #include "cmd_delete.h"
14 #include "dialog.h"
15 #include "dirpane.h"
16 #include "errors.h"
17 #include "fileutil.h"
18 #include "guiutil.h"
19 #include "overwrite.h"
20 #include "progress.h"
21 #include "sizeutil.h"
22
23 #include "cmd_join.h"
24
25 #define CMD_ID "join"
26
27 /* ----------------------------------------------------------------------------------------- */
28
29 /* 2011-10-04 - Happy Cinnamon Bun day. Splices data from <in> into <out>, in discrete chunks so we can report
30 ** progress. glib's built-in g_output_stream_splice() does it all at once, which freezes gentoo
31 ** for large files. Not acceptable. This would be a good candidate for multi-threading, in the
32 ** future. Considering the size of this, it's kind of annoying that glib doesn't have it. :|
33 */
chunked_splice(MainInfo * min,GInputStream * ins,GOutputStream * outs,gsize size,gchar * buf,gsize buf_size,GError ** error)34 static gssize chunked_splice(MainInfo *min, GInputStream *ins, GOutputStream *outs, gsize size, gchar *buf, gsize buf_size, GError **error)
35 {
36 gssize to_go = size, out_size = 0;
37
38 while(to_go > 0)
39 {
40 const gssize chunk = to_go > buf_size ? buf_size : to_go;
41 gssize got, wrote;
42
43 got = g_input_stream_read(ins, buf, chunk, NULL, error);
44 if(got != chunk)
45 break;
46 wrote = g_output_stream_write(outs, buf, chunk, NULL, error);
47 if(wrote != chunk)
48 break;
49 out_size += chunk;
50 if(pgs_progress_item_update(min, out_size) != PGS_PROCEED)
51 break;
52 }
53 return out_size;
54 }
55
56 /* 2000-02-12 - Do the actual joining of the selected files. Returns TRUE on success. */
do_join(MainInfo * min,DirPane * src,DirPane * dst,GtkListStore * store,GtkEntry * entry,gsize total_size)57 static gboolean do_join(MainInfo *min, DirPane *src, DirPane *dst, GtkListStore *store, GtkEntry *entry, gsize total_size)
58 {
59 const gchar *str = gtk_entry_get_text(entry);
60 GFile *out;
61 OvwRes owres;
62 gboolean doit = TRUE;
63 GError *err = NULL;
64 GFileOutputStream *outs;
65 gsize done_size = 0;
66
67 /* Protect against slashes in destination name. No rename-trickery! */
68 if(g_utf8_strchr(str, -1, G_DIR_SEPARATOR) != NULL)
69 {
70 err_set(min, EINVAL, CMD_ID, NULL);
71 return FALSE;
72 }
73
74 out = dp_get_file_from_name_display(dst, str);
75 ovw_overwrite_begin(min, _("\"%s\" Already Exists - Continue With Join?"), 0U);
76 owres = ovw_overwrite_unary_file(dst, out);
77 switch(owres)
78 {
79 case OVW_SKIP:
80 case OVW_CANCEL:
81 doit = FALSE;
82 break;
83 case OVW_PROCEED:
84 break;
85 case OVW_PROCEED_FILE:
86 case OVW_PROCEED_DIR:
87 doit = del_delete_gfile(min, out, FALSE, &err);
88 break;
89 }
90 if(!doit)
91 {
92 err_set_gerror(min, &err, CMD_ID, out);
93 g_object_unref(out);
94 return FALSE;
95 }
96
97 if((outs = g_file_create(out, G_FILE_CREATE_NONE, NULL, &err)) != NULL)
98 {
99 GtkTreeIter iter;
100 gssize put;
101 gchar *buf;
102 const gsize buf_size = cpy_get_buf_size();
103
104 if((buf = g_malloc(buf_size)) != NULL)
105 {
106 pgs_progress_begin(min, _("Joining..."), PFLG_BYTE_VISIBLE);
107 gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);
108 while(gtk_list_store_iter_is_valid(store, &iter))
109 {
110 GFile *in;
111 DirRow2 *dr;
112
113 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, 2, &dr, -1);
114 if((in = dp_get_file_from_row(src, dr)) != NULL)
115 {
116 GFileInputStream *ins;
117
118 if((ins = g_file_read(in, NULL, &err)) != NULL)
119 {
120 const gsize size = dp_row_get_size(dp_get_tree_model(src), dr);
121
122 pgs_progress_item_begin(min, dp_row_get_name_display(dp_get_tree_model(src), dr), size);
123 put = chunked_splice(min, G_INPUT_STREAM(ins), G_OUTPUT_STREAM(outs), size, buf, buf_size, &err);
124 if(put < 0 || !g_input_stream_close(G_INPUT_STREAM(ins), NULL, &err))
125 break;
126 pgs_progress_item_end(min);
127 dp_unselect(src, dr);
128 done_size += put;
129 }
130 g_object_unref(in);
131 }
132 else
133 break;
134 if(!gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter))
135 break;
136 }
137 pgs_progress_end(min);
138 g_free(buf);
139 }
140 if(done_size == total_size)
141 dp_rescan_post_cmd(dst);
142 }
143 if(done_size != total_size)
144 err_set_gerror(min, &err, CMD_ID, out);
145
146 g_object_unref(out);
147 ovw_overwrite_end(min);
148
149 return done_size == total_size;
150 }
151
evt_dest_changed(GtkEditable * w,gpointer user)152 static void evt_dest_changed(GtkEditable *w, gpointer user)
153 {
154 Dialog *dlg = user;
155 const gchar *str = gtk_entry_get_text(GTK_ENTRY(w));
156 gsize len;
157
158 /* Protect against empty destination name, nicely. */
159 len = strlen(str);
160 dlg_dialog_set_positive_enabled(dlg, len > 0);
161 }
162
cmd_join(MainInfo * min,DirPane * src,DirPane * dst,const CmdArg * ca)163 gint cmd_join(MainInfo *min, DirPane *src, DirPane *dst, const CmdArg *ca)
164 {
165 GSList *sel;
166 gint ret = 1;
167
168 err_clear(min);
169
170 if((sel = dp_get_selection(src)) != NULL)
171 {
172 GtkTreeModel *m = dp_get_tree_model(src);
173 GSList *iter;
174 gsize tot = 0, num = 0;
175
176 /* Compute size of all regular files in the selection; ignore 0-sized files. */
177 for(iter = sel; iter != NULL; iter = g_slist_next(iter))
178 {
179 guint64 size = dp_row_get_size(m, iter->data);
180 /* Follow symlinks, since we want to allow the joining of a bunch of link targets. */
181 if(size > 0 && dp_row_get_file_type(m, iter->data, TRUE) == G_FILE_TYPE_REGULAR)
182 {
183 tot += size;
184 num++;
185 }
186 }
187
188 /* Only open the dialog and proceed with command if there were, in
189 ** fact, more than one regular file selected.
190 */
191 if(num > 1)
192 {
193 Dialog *dlg;
194 GtkWidget *scwin, *vbox, *label, *entry, *view;
195 GtkListStore *store;
196 GtkCellRenderer *cr, *sr;
197 GtkTreeViewColumn *vc;
198 GtkTreeSelection *treesel;
199 gchar buf[FILENAME_MAX + 64];
200 const gchar *first = NULL, *tail;
201 gunichar ch;
202
203 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
204 label = gtk_label_new(_("Click and Drag Files to Reorder, Then Click Join."));
205 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
206
207 if(tot > (1 << 10))
208 {
209 gchar sbuf[16], bbuf[32];
210
211 sze_put_offset(sbuf, sizeof sbuf, tot, SZE_AUTO, 3, ',');
212 sze_put_offset(bbuf, sizeof bbuf, tot, SZE_BYTES, 3, ',');
213 g_snprintf(buf, sizeof buf, _("The total size is %s (%s)."), sbuf, bbuf);
214 }
215 else
216 g_snprintf(buf, sizeof buf, _("The total size is %lu bytes."), (unsigned long) tot);
217 label = gtk_label_new(buf);
218 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
219
220 scwin = gtk_scrolled_window_new(NULL, NULL);
221 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scwin), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
222
223 /* Create and initialize list store with selected files and their sizes. */
224 store = gtk_list_store_new(3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER);
225 for(iter = sel; iter != NULL; iter = g_slist_next(iter))
226 {
227 if(dp_row_get_file_type(m, iter->data, TRUE) == G_FILE_TYPE_REGULAR && dp_row_get_size(m, iter->data) > 0)
228 {
229 GtkTreeIter ti;
230 gchar bbuf[32];
231
232 sze_put_offset(bbuf, sizeof bbuf, dp_row_get_size(m, iter->data), SZE_BYTES_NO_UNIT, 3, ',');
233 gtk_list_store_insert_with_values(store, &ti, -1,
234 0, dp_row_get_name_display(m, iter->data),
235 1, bbuf,
236 2, iter->data,
237 -1);
238 if(first == NULL)
239 first = dp_row_get_name_display(m, iter->data);
240 }
241 }
242
243 view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
244 cr = gtk_cell_renderer_text_new();
245 vc = gtk_tree_view_column_new_with_attributes("(title)", cr, "text", 0, NULL);
246 gtk_tree_view_append_column(GTK_TREE_VIEW(view), vc);
247 sr = gtk_cell_renderer_text_new();
248 g_object_set(G_OBJECT(sr), "xalign", 1.0f, NULL);
249 vc = gtk_tree_view_column_new_with_attributes("(title)", sr, "text", 1, NULL);
250 gtk_tree_view_append_column(GTK_TREE_VIEW(view), vc);
251 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(view), FALSE);
252
253 treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
254 gtk_tree_selection_set_mode(treesel, GTK_SELECTION_NONE);
255 gtk_tree_view_set_reorderable(GTK_TREE_VIEW(view), TRUE);
256 gtk_widget_set_size_request(view, 0, 256); /* Get a sensible minmum height. */
257 gtk_container_add(GTK_CONTAINER(scwin), view);
258 gtk_box_pack_start(GTK_BOX(vbox), scwin, TRUE, TRUE, 0);
259
260 label = gtk_label_new(_("Enter Destination File Name"));
261 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 5);
262 entry = gui_dialog_entry_new();
263 gtk_entry_set_max_length(GTK_ENTRY(entry), FILENAME_MAX - 1);
264 /* Get a pointer to the terminating '\0' character. */
265 tail = g_utf8_offset_to_pointer(first, g_utf8_strlen(first, -1));
266 do
267 {
268 tail = g_utf8_find_prev_char(first, tail); /* Step backward. */
269 ch = g_utf8_get_char(tail);
270 } while(g_unichar_isxdigit(ch) || ch == '.');
271 gtk_entry_buffer_set_text(gtk_entry_get_buffer(GTK_ENTRY(entry)), first, g_utf8_pointer_to_offset(first, tail) + 1);
272 gtk_editable_set_position(GTK_EDITABLE(entry), -1);
273 gtk_box_pack_start(GTK_BOX(vbox), entry, FALSE, FALSE, 0);
274
275 dlg = dlg_dialog_sync_new(vbox, _("Join"), _("_Join|_Cancel"));
276 g_signal_connect(G_OBJECT(entry), "changed", G_CALLBACK(evt_dest_changed), dlg);
277 gtk_widget_show_all(vbox);
278 gtk_widget_grab_focus(entry);
279 if(dlg_dialog_sync_wait(dlg) == DLG_POSITIVE)
280 ret = do_join(min, src, dst, store, GTK_ENTRY(entry), tot);
281 dlg_dialog_sync_destroy(dlg);
282 }
283 dp_free_selection(sel);
284 }
285 return ret;
286 }
287