1 /*
2  * ROX-Filer, filer for the ROX desktop project
3  * Copyright (C) 2006, Thomas Leonard and others (see changelog for details).
4  *
5  * This program is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License as published by the Free
7  * Software Foundation; either version 2 of the License, or (at your option)
8  * any later version.
9  *
10  * This program is distributed in the hope that it will be useful, but WITHOUT
11  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
13  * more details.
14  *
15  * You should have received a copy of the GNU General Public License along with
16  * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
17  * Place, Suite 330, Boston, MA  02111-1307  USA
18  */
19 
20 /* bulk_rename.c - rename multiple files at once */
21 
22 #include "config.h"
23 
24 #include <stdlib.h>
25 #include <gtk/gtk.h>
26 #include <sys/types.h>
27 #include <regex.h>
28 #include <string.h>
29 #include <errno.h>
30 
31 #include "global.h"
32 
33 #include "main.h"
34 #include "bulk_rename.h"
35 #include "support.h"
36 #include "gui_support.h"
37 
38 enum {RESPONSE_RENAME, RESPONSE_RESET};
39 
40 /* Static prototypes */
41 static gboolean apply_replace(GtkWidget *box);
42 static void response(GtkWidget *box, int resp, GtkListStore *model);
43 static void reset_model(GtkListStore *model);
44 static gboolean rename_items(const char *dir, GtkListStore *list);
45 static void cell_edited(GtkCellRendererText *cell, const gchar *path_string,
46 			const gchar *new_text, GtkTreeModel *model);
47 
48 
49 /****************************************************************
50  *			EXTERNAL INTERFACE			*
51  ****************************************************************/
52 
53 /* Bulk rename these items */
bulk_rename(const char * dir,GList * items)54 void bulk_rename(const char *dir, GList *items)
55 {
56 	GtkWidget *box, *button, *tree, *swin, *hbox;
57 	GtkWidget *replace_entry, *with_entry;
58 	GtkTreeViewColumn *column;
59 	GtkCellRenderer *cell_renderer;
60 	GtkListStore *model;
61 	GtkRequisition req;
62 
63 	box = gtk_dialog_new();
64 	g_object_set_data_full(G_OBJECT(box), "rename_dir",
65 				g_strdup(dir), g_free);
66 	gtk_window_set_title(GTK_WINDOW(box), _("Bulk rename files"));
67 	gtk_dialog_set_has_separator(GTK_DIALOG(box), FALSE);
68 
69 	button = button_new_mixed(GTK_STOCK_REFRESH, _("Reset"));
70 	GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
71 	gtk_dialog_add_action_widget(GTK_DIALOG(box), button, RESPONSE_RESET);
72 	gtk_dialog_set_default_response(GTK_DIALOG(box), RESPONSE_RESET);
73 	gtk_tooltips_set_tip(tooltips, button,
74 			_("Make the New column a copy of Old"), NULL);
75 
76 	gtk_dialog_add_button(GTK_DIALOG(box),
77 				GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL);
78 
79 	button = button_new_mixed(GTK_STOCK_EXECUTE, _("_Rename"));
80 	GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
81 	gtk_dialog_add_action_widget(GTK_DIALOG(box), button, RESPONSE_RENAME);
82 	gtk_dialog_set_default_response(GTK_DIALOG(box), RESPONSE_RENAME);
83 
84 	/* Replace */
85 
86 	hbox = gtk_hbox_new(FALSE, 4);
87 	gtk_container_set_border_width(GTK_CONTAINER(hbox), 4);
88 	gtk_box_pack_start(GTK_BOX(GTK_DIALOG(box)->vbox),
89 				hbox, FALSE, TRUE, 0);
90 
91 	gtk_box_pack_start(GTK_BOX(hbox),
92 				gtk_label_new(_("Replace:")), FALSE, TRUE, 0);
93 
94 	replace_entry = gtk_entry_new();
95 	g_object_set_data(G_OBJECT(box), "replace_entry", replace_entry);
96 	gtk_box_pack_start(GTK_BOX(hbox), replace_entry, TRUE, TRUE, 0);
97 	gtk_entry_set_text(GTK_ENTRY(replace_entry), "\\.htm$");
98 	gtk_tooltips_set_tip(tooltips, replace_entry,
99 			_("This is a regular expression to search for.\n"
100 			"^ matches the start of a filename\n"
101 			"$ matches the end\n"
102 			"\\. matches a dot\n"
103 			"\\.htm$ matches the '.htm' in 'index.htm', etc"),
104 			NULL);
105 
106 	gtk_box_pack_start(GTK_BOX(hbox),
107 				gtk_label_new(_("With:")), FALSE, TRUE, 0);
108 
109 	with_entry = gtk_entry_new();
110 	g_object_set_data(G_OBJECT(box), "with_entry", with_entry);
111 	gtk_box_pack_start(GTK_BOX(hbox), with_entry, TRUE, TRUE, 0);
112 	gtk_entry_set_text(GTK_ENTRY(with_entry), ".html");
113 	gtk_tooltips_set_tip(tooltips, with_entry,
114 			_("The first match in each filename will be replaced "
115 			"by this string. "
116 			"The only special characters are back-references "
117 			"from \\0 to \\9. To use them literally, "
118 			"they have to be escaped with a backslash."), NULL);
119 
120 	button = gtk_button_new_with_label(_("Apply"));
121 	gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, TRUE, 0);
122 	gtk_tooltips_set_tip(tooltips, button,
123 			_("Do a search-and-replace in the New column. "
124 			"The files are not actually renamed until you click "
125 			"on the Rename button below."), NULL);
126 
127 	g_signal_connect_swapped(replace_entry, "activate",
128 			G_CALLBACK(gtk_widget_grab_focus), with_entry);
129 	g_signal_connect_swapped(with_entry, "activate",
130 			G_CALLBACK(apply_replace), box);
131 	g_signal_connect_swapped(button, "clicked",
132 			G_CALLBACK(apply_replace), box);
133 
134 	/* The TreeView */
135 
136 	model = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
137 	g_object_set_data(G_OBJECT(box), "tree_model", model);
138 	tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(model));
139 
140 	cell_renderer = gtk_cell_renderer_text_new();
141 	column = gtk_tree_view_column_new_with_attributes(
142 				_("Old name"), cell_renderer, "text", 0, NULL);
143 	gtk_tree_view_column_set_resizable(column, TRUE);
144 	gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
145 
146 	cell_renderer = gtk_cell_renderer_text_new();
147 	g_object_set(G_OBJECT(cell_renderer), "editable", TRUE, NULL);
148 	g_signal_connect(G_OBJECT(cell_renderer), "edited",
149 			G_CALLBACK(cell_edited), model);
150 	column = gtk_tree_view_column_new_with_attributes(
151 				_("New name"), cell_renderer, "text", 1, NULL);
152 	gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
153 
154 	swin = gtk_scrolled_window_new(NULL, NULL);
155 	gtk_container_set_border_width(GTK_CONTAINER(swin), 4);
156 	gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(swin),
157 						GTK_SHADOW_IN);
158 	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swin),
159 				GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
160 	gtk_box_pack_start(GTK_BOX(GTK_DIALOG(box)->vbox), swin, TRUE, TRUE, 0);
161 	gtk_container_add(GTK_CONTAINER(swin), tree);
162 
163 	while (items) {
164 		GtkTreeIter iter;
165 		const char *name = items->data;
166 
167 		gtk_list_store_append(model, &iter);
168 		gtk_list_store_set(model, &iter, 0, name, 1, name, -1);
169 
170 		items = items->next;
171 	}
172 
173 	gtk_widget_show_all(tree);
174 	gtk_widget_size_request(tree, &req);
175 	req.width = MIN(req.width + 50, screen_width - 50);
176 	req.height = MIN(req.height + 150, screen_height - 50);
177 
178 	gtk_window_set_default_size(GTK_WINDOW(box), req.width, req.height);
179 
180 	number_of_windows++;
181 	g_signal_connect(box, "destroy", G_CALLBACK(one_less_window), NULL);
182 	g_signal_connect(box, "response", G_CALLBACK(response), model);
183 	gtk_widget_show_all(box);
184 }
185 
186 /****************************************************************
187  *			INTERNAL FUNCTIONS			*
188  ****************************************************************/
189 
response(GtkWidget * box,int resp,GtkListStore * model)190 static void response(GtkWidget *box, int resp, GtkListStore *model)
191 {
192 	if (resp == RESPONSE_RESET)
193 		reset_model(model);
194 	else if (resp == RESPONSE_RENAME)
195 	{
196 		if (rename_items(g_object_get_data(G_OBJECT(box), "rename_dir"),
197 				 model))
198 			gtk_widget_destroy(box);
199 	}
200 	else
201 		gtk_widget_destroy(box);
202 }
203 
204 /** Substitute: s/old/with/
205  * Returns the result as a new string, or NULL if there is no match.
206  * "replace" is a compiled version of "with".
207  * Caller must free the result.
208  */
subst(const char * old,regex_t * replace,const char * with)209 static GString *subst(const char *old, regex_t *replace, const char *with)
210 {
211 	int max_subs = 10;
212 	GString *new;
213 	regmatch_t match[max_subs];
214 
215 	if (regexec(replace, old, max_subs, match, 0) != 0)
216 		return NULL;		/* No match */
217 
218 	g_return_val_if_fail(match[0].rm_so != -1, NULL);
219 
220 	new = g_string_new(NULL);
221 	g_string_append_len(new, old, match[0].rm_so);
222 
223 	int i;
224 	for (i = 0; with[i]; i++)
225 	{
226 		if (with[i] == '\\' && with[i+1])
227 		{
228 			i++;
229 			if (with[i] >= '0' && with[i]-'0' < max_subs)
230 			{
231 				int subpat;
232 
233 				subpat = with[i] - '0';
234 
235 				if (match[subpat].rm_so != -1)
236 					g_string_append_len(new, old + match[subpat].rm_so,
237 							match[subpat].rm_eo - match[subpat].rm_so);
238 
239 			}
240 			else
241 			{
242 				// Escape next character
243 				g_string_append_c(new, with[i]);
244 			}
245 		}
246 		else
247 			g_string_append_c(new, with[i]);
248 	}
249 
250 	g_string_append(new, old + match[0].rm_eo);
251 
252 	return new;
253 }
254 
255 /* Do a search-and-replace on the second column. */
update_model(GtkListStore * list,regex_t * replace,const char * with)256 static void update_model(GtkListStore *list, regex_t *replace, const char *with)
257 {
258 	GtkTreeIter iter;
259 	GtkTreeModel *model = (GtkTreeModel *) list;
260 	int n_matched = 0;
261 	int n_changed = 0;
262 
263 	if (!gtk_tree_model_get_iter_first(model, &iter))
264 	{
265 		g_warning("Model empty!");
266 		return;
267 	}
268 
269 	do
270 	{
271 		GString *new;
272 		char *old = NULL;
273 
274 		gtk_tree_model_get(model, &iter, 1, &old, -1);
275 
276 		new = subst(old, replace, with);
277 		if (new)
278 		{
279 			n_matched++;
280 			if (strcmp(old, new->str) != 0)
281 			{
282 				n_changed++;
283 				gtk_list_store_set(list, &iter, 1, new->str, -1);
284 			}
285 
286 			g_string_free(new, TRUE);
287 		}
288 		g_free(old);
289 
290 	} while (gtk_tree_model_iter_next(model, &iter));
291 
292 	if (n_matched == 0)
293 		report_error(_("No strings (in the New column) matched "
294 				"the given expression"));
295 	else if (n_changed == 0)
296 	{
297 		if (n_matched == 1)
298 			report_error(_("One name matched, but the result was "
299 					"the same"));
300 		else
301 			report_error(_("%d names matched, but the results were "
302 					"all the same"), n_matched);
303 	}
304 }
305 
apply_replace(GtkWidget * box)306 static gboolean apply_replace(GtkWidget *box)
307 {
308 	GtkListStore *model;
309 	GtkEntry *replace_entry, *with_entry;
310 	const char *replace, *with;
311 	regex_t compiled;
312 	int error;
313 
314 	replace_entry = g_object_get_data(G_OBJECT(box), "replace_entry");
315 	with_entry = g_object_get_data(G_OBJECT(box), "with_entry");
316 	model = g_object_get_data(G_OBJECT(box), "tree_model");
317 
318 	g_return_val_if_fail(replace_entry != NULL, TRUE);
319 	g_return_val_if_fail(with_entry != NULL, TRUE);
320 	g_return_val_if_fail(model != NULL, TRUE);
321 
322 	replace = gtk_entry_get_text(replace_entry);
323 	with = gtk_entry_get_text(with_entry);
324 
325 	if (replace[0] == '\0' && with[0] == '\0')
326 	{
327 		report_error(_("Specify a regular expression to match, "
328 				"and a string to replace matches with."));
329 		return TRUE;
330 	}
331 
332 	error = regcomp(&compiled, replace, REG_EXTENDED);
333 	if (error)
334 	{
335 		char *message;
336 		size_t size;
337 
338 		size = regerror(error, &compiled, NULL, 0);
339 		g_return_val_if_fail(size > 0, TRUE);
340 
341 		message = g_malloc(size);
342 		regerror(error, &compiled, message, size);
343 
344 		report_error(_("%s (for '%s')"), message, replace);
345 
346 		return TRUE;
347 	}
348 
349 	update_model(model, &compiled, with);
350 
351 	regfree(&compiled);
352 
353 	return TRUE;
354 }
355 
reset_model(GtkListStore * list)356 static void reset_model(GtkListStore *list)
357 {
358 	GtkTreeIter iter;
359 	GtkTreeModel *model = (GtkTreeModel *) list;
360 
361 	if (!gtk_tree_model_get_iter_first(model, &iter))
362 		return;
363 
364 	do {
365 		char *before;
366 		gtk_tree_model_get(model, &iter, 0, &before, -1);
367 		gtk_list_store_set(list, &iter, 1, before, -1);
368 		g_free(before);
369 	} while (gtk_tree_model_iter_next(model, &iter));
370 }
371 
do_rename(const char * before,const char * after)372 static gboolean do_rename(const char *before, const char *after)
373 {
374 	/* Check again, just in case */
375 	if (access(after, F_OK) == 0)
376 	{
377 		report_error(_("A file called '%s' already exists. "
378 				"Aborting bulk rename."), after);
379 	}
380 	else if (rename(before, after))
381 	{
382 		report_error(_("Failed to rename '%s' as '%s':\n%s\n"
383 				"Aborting bulk rename."), before, after,
384 				g_strerror(errno));
385 	}
386 	else
387 		return TRUE;
388 	return FALSE;
389 }
390 
rename_items(const char * dir,GtkListStore * list)391 static gboolean rename_items(const char *dir, GtkListStore *list)
392 {
393 	GtkTreeModel *model = (GtkTreeModel *) list;
394 	GtkTreeIter iter;
395 	char *slash_example = NULL;
396 	GHashTable *names = NULL;
397 	gboolean success = FALSE;
398 	int n_renames = 0;
399 
400 	g_return_val_if_fail(dir != NULL, FALSE);
401 	g_return_val_if_fail(list != NULL, FALSE);
402 
403 	if (!gtk_tree_model_get_iter_first(model, &iter))
404 		return FALSE;	/* (error) */
405 
406 	names = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
407 	do {
408 		char *before, *after;
409 		const char *dest;
410 
411 		gtk_tree_model_get(model, &iter, 0, &before, 1, &after, -1);
412 
413 		if (!slash_example && strchr(after, '/'))
414 		{
415 			slash_example = g_strdup(after);
416 		}
417 
418 		if (g_hash_table_lookup(names, before))
419 		{
420 			report_error("Filename '%s' used twice!", before);
421 			goto fail;
422 		}
423 		g_hash_table_insert(names, before, "");
424 
425 		if (after[0] == '\0' || strcmp(after, before) == 0)
426 		{
427 			g_free(after);
428 			continue;
429 		}
430 
431 		if (g_hash_table_lookup(names, after))
432 		{
433 			report_error("Filename '%s' used twice!", after);
434 			goto fail;
435 		}
436 		g_hash_table_insert(names, after, "");
437 
438 		if (after[0] == '/')
439 			dest = after;
440 		else
441 			dest = make_path(dir, after);
442 		if (access(dest, F_OK) == 0)
443 		{
444 			report_error(_("A file called '%s' already exists"),
445 					dest);
446 			goto fail;
447 		}
448 
449 		n_renames++;
450 	} while (gtk_tree_model_iter_next(model, &iter));
451 
452 	if (slash_example)
453 	{
454 		char *message;
455 		message = g_strdup_printf(_("Some of the New names contain "
456 				"/ characters (eg '%s'). "
457 				"This will cause the files to end up in "
458 				"different directories. "
459 				"Continue?"), slash_example);
460 		if (!confirm(message, GTK_STOCK_EXECUTE, "Rename anyway"))
461 		{
462 			g_free(message);
463 			goto fail;
464 		}
465 		g_free(message);
466 	}
467 
468 	if (n_renames == 0)
469 	{
470 		report_error(_("None of the names have changed. "
471 				"Nothing to do!"));
472 		goto fail;
473 	}
474 
475 	success = TRUE;
476 	gtk_tree_model_get_iter_first(model, &iter);
477 	while (success)
478 	{
479 		char *before, *after, *before_path;
480 		const char *dest;
481 
482 		gtk_tree_model_get(model, &iter, 0, &before, 1, &after, -1);
483 
484 		if (after[0] == '\0' || strcmp(after, before) == 0)
485 			dest = NULL;
486 		else if (after[0] == '/')
487 			dest = after;
488 		else
489 			dest = make_path(dir, after);
490 
491 		before_path = g_build_filename(dir, before, NULL);
492 
493 		if (dest == NULL || do_rename(before_path, dest))
494 		{
495 			/* Advances iter */
496 			if (!gtk_list_store_remove(list, &iter))
497 				break;	/* Last item; finished */
498 		}
499 		else
500 			success = FALSE;
501 
502 		g_free(before_path);
503 		g_free(before);
504 		g_free(after);
505 	}
506 
507 fail:
508 	g_free(slash_example);
509 	if (names)
510 		g_hash_table_destroy(names);
511 	return success;
512 }
513 
cell_edited(GtkCellRendererText * cell,const gchar * path_string,const gchar * new_text,GtkTreeModel * model)514 static void cell_edited(GtkCellRendererText *cell,
515 			const gchar *path_string, const gchar *new_text,
516 		        GtkTreeModel *model)
517 {
518 	GtkTreePath *path;
519 	GtkTreeIter iter;
520 
521 	path = gtk_tree_path_new_from_string(path_string);
522 	gtk_tree_model_get_iter(model, &iter, path);
523 	gtk_tree_path_free(path);
524 
525 	gtk_list_store_set(GTK_LIST_STORE(model), &iter, 1, new_text, -1);
526 }
527 
528 #ifdef UNIT_TESTS
test_subst(const char * string,const char * pattern,const char * with,const char * expected)529 static void test_subst(const char *string, const char *pattern, const char *with, const char *expected)
530 {
531 	regex_t compiled;
532 	GString *new;
533 
534 	g_print("Testing s/%s/%s\n", pattern, with);
535 
536 	if (regcomp(&compiled, pattern, REG_EXTENDED))
537 		g_error("Failed to compiled '%s'", pattern);
538 
539 	new = subst(string, &compiled, with);
540 
541 	if (new == NULL)
542 	{
543 		g_return_if_fail(expected == NULL);
544 	}
545 	else
546 	{
547 		//g_print("Got: %s\n", new->str);
548 		g_return_if_fail(expected != NULL);
549 		g_return_if_fail(strcmp(new->str, expected) == 0);
550 		g_string_free(new, TRUE);
551 	}
552 
553 	regfree(&compiled);
554 }
555 
bulk_rename_tests()556 void bulk_rename_tests()
557 {
558 	test_subst("hello", "l", "L", "heLlo");
559 	test_subst("hello", "h(.*)l", "\\1-\\1", "el-elo");
560 	test_subst("hello", "(h)", "\\1", "hello");
561 	test_subst("hello", "(h)", "\\\\1", "\\1ello");
562 	test_subst("hello", "(h)", "\\\\\\1", "\\hello");
563 	test_subst("hello", "(h)", "\\", "\\ello");
564 	test_subst("hello", "(h)$", "\\", NULL);
565 	test_subst("hello", "(.)(.)(.).*", "\\0-\\1-\\2-\\3-\\4-\\9-\\:", "hello-h-e-l---:");
566 }
567 #endif
568