1 /*
2 * fm-deep-count-job.c
3 *
4 * Copyright 2009 - 2012 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
5 * Copyright 2013-2014 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
6 *
7 * This file is a part of the Libfm library.
8 *
9 * This library is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU Lesser General Public
11 * License as published by the Free Software Foundation; either
12 * version 2.1 of the License, or (at your option) any later version.
13 *
14 * This library is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 * Lesser General Public License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this library; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
22 */
23
24 /**
25 * SECTION:fm-deep-count-job
26 * @short_description: Job to gather information about file sizes.
27 * @title: FmDeepCountJob
28 *
29 * @include: libfm/fm.h
30 *
31 * The #FmDeepCountJob can be used to recursively gather information about
32 * some files and directories content before copy or move. It counts total
33 * size of all given files and directories, and size on disk for them.
34 * If flags for the job include FM_DC_JOB_PREPARE_MOVE then also count of
35 * files to move between volumes will be counted as well.
36 */
37
38 #include "fm-deep-count-job.h"
39 #include <glib/gstdio.h>
40 #include <errno.h>
41
42 static void fm_deep_count_job_dispose (GObject *object);
43 G_DEFINE_TYPE(FmDeepCountJob, fm_deep_count_job, FM_TYPE_JOB);
44
45 static gboolean fm_deep_count_job_run(FmJob* job);
46
47 static gboolean deep_count_posix(FmDeepCountJob* job, const char* path);
48 static gboolean deep_count_gio(FmDeepCountJob* job, GFileInfo* inf, GFile* gf);
49
50 static const char query_str[] =
51 G_FILE_ATTRIBUTE_STANDARD_TYPE","
52 G_FILE_ATTRIBUTE_STANDARD_NAME","
53 G_FILE_ATTRIBUTE_STANDARD_IS_VIRTUAL","
54 G_FILE_ATTRIBUTE_STANDARD_SIZE","
55 G_FILE_ATTRIBUTE_STANDARD_ALLOCATED_SIZE","
56 G_FILE_ATTRIBUTE_ID_FILESYSTEM;
57
fm_deep_count_job_class_init(FmDeepCountJobClass * klass)58 static void fm_deep_count_job_class_init(FmDeepCountJobClass *klass)
59 {
60 GObjectClass *g_object_class;
61 FmJobClass* job_class;
62 g_object_class = G_OBJECT_CLASS(klass);
63 g_object_class->dispose = fm_deep_count_job_dispose;
64 /* use finalize from parent class */
65
66 job_class = FM_JOB_CLASS(klass);
67 job_class->run = fm_deep_count_job_run;
68 }
69
70
fm_deep_count_job_dispose(GObject * object)71 static void fm_deep_count_job_dispose(GObject *object)
72 {
73 FmDeepCountJob *self;
74
75 g_return_if_fail(object != NULL);
76 g_return_if_fail(FM_IS_DEEP_COUNT_JOB(object));
77
78 self = (FmDeepCountJob*)object;
79
80 if(self->paths)
81 {
82 fm_path_list_unref(self->paths);
83 self->paths = NULL;
84 }
85 G_OBJECT_CLASS(fm_deep_count_job_parent_class)->dispose(object);
86 }
87
88
fm_deep_count_job_init(FmDeepCountJob * self)89 static void fm_deep_count_job_init(FmDeepCountJob *self)
90 {
91 fm_job_init_cancellable(FM_JOB(self));
92 }
93
94 /**
95 * fm_deep_count_job_new
96 * @paths: list of files and directories to count sizes
97 * @flags: flags of the counting behavior
98 *
99 * Creates a new #FmDeepCountJob which can be ran via #FmJob API.
100 *
101 * Returns: (transfer full): a new #FmDeepCountJob object.
102 *
103 * Since: 0.1.0
104 */
fm_deep_count_job_new(FmPathList * paths,FmDeepCountJobFlags flags)105 FmDeepCountJob *fm_deep_count_job_new(FmPathList* paths, FmDeepCountJobFlags flags)
106 {
107 FmDeepCountJob* job = (FmDeepCountJob*)g_object_new(FM_DEEP_COUNT_JOB_TYPE, NULL);
108 job->paths = fm_path_list_ref(paths);
109 job->flags = flags;
110 return job;
111 }
112
fm_deep_count_job_run(FmJob * job)113 static gboolean fm_deep_count_job_run(FmJob* job)
114 {
115 FmDeepCountJob* dc = (FmDeepCountJob*)job;
116 GList* l;
117
118 l = fm_path_list_peek_head_link(dc->paths);
119 for(; !fm_job_is_cancelled(job) && l; l=l->next)
120 {
121 FmPath* path = FM_PATH(l->data);
122 if(fm_path_is_native(path)) /* if it's a native file, use posix APIs */
123 {
124 char *path_str = fm_path_to_str(path);
125 deep_count_posix( dc, path_str );
126 g_free(path_str);
127 }
128 else
129 {
130 GFile* gf = fm_path_to_gfile(path);
131 deep_count_gio( dc, NULL, gf );
132 g_object_unref(gf);
133 }
134 }
135 return TRUE;
136 }
137
deep_count_posix(FmDeepCountJob * job,const char * path)138 static gboolean deep_count_posix(FmDeepCountJob* job, const char *path)
139 {
140 FmJob* fmjob = FM_JOB(job);
141 struct stat st;
142 int ret;
143
144 _retry_stat:
145 if( G_UNLIKELY(job->flags & FM_DC_JOB_FOLLOW_LINKS) )
146 ret = stat(path, &st);
147 else
148 ret = lstat(path, &st);
149
150 if( ret == 0 )
151 {
152 ++job->count;
153 /* SF bug #892: dir file size is not relevant in the summary */
154 if (!S_ISDIR(st.st_mode))
155 job->total_size += (goffset)st.st_size;
156 job->total_ondisk_size += (st.st_blocks * 512);
157
158 /* NOTE: if job->dest_dev is 0, that means our destination
159 * folder is not on native UNIX filesystem. Hence it's not
160 * on the same device. Our st.st_dev will always be non-zero
161 * since our file is on a native UNIX filesystem. */
162
163 /* only descends into files on the same filesystem */
164 if( job->flags & FM_DC_JOB_SAME_FS )
165 {
166 if( st.st_dev != job->dest_dev )
167 return TRUE;
168 }
169 /* only descends into files on the different filesystem */
170 else if( job->flags & FM_DC_JOB_PREPARE_MOVE )
171 {
172 if( st.st_dev == job->dest_dev )
173 return TRUE;
174 }
175 }
176 else
177 {
178 GError* err = g_error_new(G_IO_ERROR, g_io_error_from_errno(errno), "%s", g_strerror(errno));
179 FmJobErrorAction act = fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MILD);
180 g_error_free(err);
181 err = NULL;
182 if(act == FM_JOB_RETRY)
183 goto _retry_stat;
184 return FALSE;
185 }
186 if(fm_job_is_cancelled(fmjob))
187 return FALSE;
188
189 if( S_ISDIR(st.st_mode) ) /* if it's a dir */
190 {
191 GDir* dir_ent = g_dir_open(path, 0, NULL);
192 if(dir_ent)
193 {
194 const char* basename;
195 while( !fm_job_is_cancelled(fmjob)
196 && (basename = g_dir_read_name(dir_ent)) )
197 {
198 char *sub = g_build_filename(path, basename, NULL);
199 if(!fm_job_is_cancelled(fmjob))
200 {
201 if(deep_count_posix(job, sub))
202 {
203 /* for moving across different devices, an additional 'delete'
204 * for source file is needed. so let's +1 for the delete.*/
205 if(job->flags & FM_DC_JOB_PREPARE_MOVE)
206 {
207 ++job->total_size;
208 ++job->total_ondisk_size;
209 ++job->count;
210 }
211 }
212 }
213 g_free(sub);
214 }
215 g_dir_close(dir_ent);
216 }
217 }
218 return TRUE;
219 }
220
deep_count_gio(FmDeepCountJob * job,GFileInfo * inf,GFile * gf)221 static gboolean deep_count_gio(FmDeepCountJob* job, GFileInfo* inf, GFile* gf)
222 {
223 FmJob* fmjob = FM_JOB(job);
224 GError* err = NULL;
225 GFileType type;
226 const char* fs_id;
227 gboolean descend;
228
229 if(inf)
230 g_object_ref(inf);
231 else
232 {
233 _retry_query_info:
234 inf = g_file_query_info(gf, query_str,
235 (job->flags & FM_DC_JOB_FOLLOW_LINKS) ? 0 : G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
236 fm_job_get_cancellable(fmjob), &err);
237 if(!inf)
238 {
239 FmJobErrorAction act = fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MILD);
240 g_error_free(err);
241 err = NULL;
242 if(act == FM_JOB_RETRY)
243 goto _retry_query_info;
244 return FALSE;
245 }
246 }
247 if(fm_job_is_cancelled(fmjob))
248 {
249 g_object_unref(inf);
250 return FALSE;
251 }
252
253 type = g_file_info_get_file_type(inf);
254 descend = TRUE;
255
256 ++job->count;
257 /* SF bug #892: dir file size is not relevant in the summary */
258 if (type != G_FILE_TYPE_DIRECTORY)
259 job->total_size += g_file_info_get_size(inf);
260 job->total_ondisk_size += g_file_info_get_attribute_uint64(inf, G_FILE_ATTRIBUTE_STANDARD_ALLOCATED_SIZE);
261
262 /* prepare for moving across different devices */
263 if( job->flags & FM_DC_JOB_PREPARE_MOVE )
264 {
265 fs_id = g_file_info_get_attribute_string(inf, G_FILE_ATTRIBUTE_ID_FILESYSTEM);
266 if( g_strcmp0(fs_id, job->dest_fs_id) != 0 )
267 {
268 /* files on different device requires an additional 'delete' for the source file. */
269 ++job->total_size; /* this is for the additional delete */
270 ++job->total_ondisk_size;
271 ++job->count;
272 }
273 else
274 descend = FALSE;
275 }
276
277 if( type == G_FILE_TYPE_DIRECTORY )
278 {
279 FmPath* fm_path = fm_path_new_for_gfile(gf);
280 /* check if we need to decends into the dir. */
281 /* trash:/// doesn't support deleting files recursively */
282 if(job->flags & FM_DC_JOB_PREPARE_DELETE && fm_path_is_trash(fm_path) && ! fm_path_is_trash_root(fm_path))
283 descend = FALSE;
284 else
285 {
286 /* only descends into files on the same filesystem */
287 if( job->flags & FM_DC_JOB_SAME_FS )
288 {
289 fs_id = g_file_info_get_attribute_string(inf, G_FILE_ATTRIBUTE_ID_FILESYSTEM);
290 descend = (g_strcmp0(fs_id, job->dest_fs_id) == 0);
291 }
292 }
293 fm_path_unref(fm_path);
294 g_object_unref(inf);
295 inf = NULL;
296
297 if(descend)
298 {
299 GFileEnumerator* enu;
300 _retry_enum_children:
301 enu = g_file_enumerate_children(gf, query_str,
302 G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
303 fm_job_get_cancellable(fmjob), &err);
304 if(enu)
305 {
306 while( !fm_job_is_cancelled(fmjob) )
307 {
308 inf = g_file_enumerator_next_file(enu, fm_job_get_cancellable(fmjob), &err);
309 if(inf)
310 {
311 GFile* child = g_file_get_child(gf, g_file_info_get_name(inf));
312 deep_count_gio(job, inf, child);
313 g_object_unref(child);
314 g_object_unref(inf);
315 inf = NULL;
316 }
317 else
318 {
319 if(err) /* error! */
320 {
321 /* FM_JOB_RETRY is not supported */
322 /*FmJobErrorAction act = */
323 fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MILD);
324 g_error_free(err);
325 err = NULL;
326 }
327 else
328 {
329 /* EOF is reached, do nothing. */
330 break;
331 }
332 }
333 }
334 g_file_enumerator_close(enu, NULL, NULL);
335 g_object_unref(enu);
336 }
337 else
338 {
339 FmJobErrorAction act = fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MILD);
340 g_error_free(err);
341 err = NULL;
342 if(act == FM_JOB_RETRY)
343 goto _retry_enum_children;
344 }
345 }
346 }
347 else
348 g_object_unref(inf);
349
350 return TRUE;
351 }
352
353 /**
354 * fm_deep_count_job_set_dest
355 * @dc: a job to set the destination
356 * @dev: UNIX device ID
357 * @fs_id: (allow-none): filesystem id in gio format
358 *
359 * Sets destination for the job @dc that will be used when the job is
360 * ran with any of flags FM_DC_JOB_SAME_FS or FM_DC_JOB_PREPARE_MOVE.
361 * If @dev is 0 (and @fs_id is NULL) then destination device is non on
362 * native filesystem.
363 *
364 * Since: 0.1.0
365 */
fm_deep_count_job_set_dest(FmDeepCountJob * dc,dev_t dev,const char * fs_id)366 void fm_deep_count_job_set_dest(FmDeepCountJob* dc, dev_t dev, const char* fs_id)
367 {
368 dc->dest_dev = dev;
369 if(fs_id)
370 dc->dest_fs_id = g_intern_string(fs_id);
371 }
372