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