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