/* * fm-file-ops-xfer.c * * Copyright 2009 PCMan * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #include "fm-file-ops-job-xfer.h" #include "fm-file-ops-job-delete.h" #include #include #include #include #include "fm-monitor.h" static const char query[]= G_FILE_ATTRIBUTE_STANDARD_TYPE"," G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME"," G_FILE_ATTRIBUTE_STANDARD_NAME"," G_FILE_ATTRIBUTE_STANDARD_IS_VIRTUAL"," G_FILE_ATTRIBUTE_STANDARD_SIZE"," G_FILE_ATTRIBUTE_UNIX_BLOCKS"," G_FILE_ATTRIBUTE_UNIX_BLOCK_SIZE"," G_FILE_ATTRIBUTE_ID_FILESYSTEM; static void progress_cb(goffset cur, goffset total, FmFileOpsJob* job); gboolean _fm_file_ops_job_copy_file(FmFileOpsJob* job, GFile* src, GFileInfo* inf, GFile* dest) { /* FIXME: prevent copying to self or copying parent dir to child. */ gboolean ret = FALSE; GError* err = NULL; gboolean is_virtual; GFileType type; guint64 size; GFile* new_dest = NULL; GFileCopyFlags flags; FmJob* fmjob = FM_JOB(job); guint32 mode; if( G_LIKELY(inf) ) g_object_ref(inf); else { _retry_query_src_info: inf = g_file_query_info(src, query, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, fm_job_get_cancellable(fmjob), &err); if( !inf ) { FmJobErrorAction act = fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MODERATE); g_error_free(err); err = NULL; if(act == FM_JOB_RETRY) goto _retry_query_src_info; return FALSE; } } /* showing currently processed file. */ fm_file_ops_job_emit_cur_file(job, g_file_info_get_display_name(inf)); is_virtual = g_file_info_get_attribute_boolean(inf, G_FILE_ATTRIBUTE_STANDARD_IS_VIRTUAL); type = g_file_info_get_file_type(inf); size = g_file_info_get_size(inf); mode = g_file_info_get_attribute_uint32(inf, G_FILE_ATTRIBUTE_UNIX_MODE); g_object_unref(inf); inf = NULL; switch(type) { case G_FILE_TYPE_DIRECTORY: { GFileEnumerator* enu; _retry_mkdir: if( !fm_job_is_cancelled(fmjob) && !g_file_make_directory(dest, fm_job_get_cancellable(fmjob), &err) ) { FmFileOpOption opt = 0; if(err->domain == G_IO_ERROR && err->code == G_IO_ERROR_EXISTS) { g_error_free(err); err = NULL; g_object_ref(dest); /*if dest == new_dest, it will become invalid when we unref new_dest */ if(new_dest) { g_object_unref(new_dest); new_dest = NULL; } opt = fm_file_ops_job_ask_rename(job, src, NULL, dest, &new_dest); g_object_unref(dest); switch(opt) { case FM_FILE_OP_RENAME: dest = new_dest; goto _retry_mkdir; break; case FM_FILE_OP_SKIP: /* when a dir is skipped, we need to know its total size to calculate correct progress */ job->finished += size; fm_file_ops_job_emit_percent(job); job->skip_dir_content = TRUE; ret = FALSE; break; case FM_FILE_OP_OVERWRITE: break; case FM_FILE_OP_CANCEL: fm_job_cancel(FM_JOB(job)); ret = FALSE; break; } } else if(!fm_job_is_cancelled(fmjob)) { FmJobErrorAction act = fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MODERATE); g_error_free(err); err = NULL; if(act == FM_JOB_RETRY) goto _retry_mkdir; } job->finished += size; fm_file_ops_job_emit_percent(job); } else { /* chmod the newly created dir properly */ if(!fm_job_is_cancelled(fmjob)) { if(mode) { _retry_chmod_for_dir: mode |= (S_IRUSR|S_IWUSR); /* ensure we have rw permission to this file. */ if( !g_file_set_attribute_uint32(dest, G_FILE_ATTRIBUTE_UNIX_MODE, mode, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, fm_job_get_cancellable(job), &err) ) { FmJobErrorAction act = fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MODERATE); g_error_free(err); err = NULL; if(act == FM_JOB_RETRY) goto _retry_chmod_for_dir; } } } job->finished += size; fm_file_ops_job_emit_percent(job); if(job->dest_folder_mon) g_file_monitor_emit_event(job->dest_folder_mon, dest, NULL, G_FILE_MONITOR_EVENT_CREATED); } /* the dest dir is created. let's copy its content. */ if(!fm_job_is_cancelled(fmjob)) { _retry_enum_children: enu = g_file_enumerate_children(src, query, 0, fm_job_get_cancellable(fmjob), &err); if(enu) { while( !fm_job_is_cancelled(fmjob) ) { inf = g_file_enumerator_next_file(enu, fm_job_get_cancellable(fmjob), &err); if( inf ) { /* don't overwrite dir content, only calculate progress. */ if(G_UNLIKELY(job->skip_dir_content)) { job->finished += g_file_info_get_size(inf); fm_file_ops_job_emit_percent(job); } else { gboolean ret2; GFileMonitor* old_dest_mon = job->dest_folder_mon; GFile* sub = g_file_get_child(src, g_file_info_get_name(inf)); GFile* sub_dest = g_file_get_child(dest, g_file_info_get_name(inf)); if(g_file_is_native(dest)) job->dest_folder_mon = NULL; else job->dest_folder_mon = fm_monitor_lookup_dummy_monitor(dest); ret2 = _fm_file_ops_job_copy_file(job, sub, inf, sub_dest); g_object_unref(sub); g_object_unref(sub_dest); if(job->dest_folder_mon) g_object_unref(job->dest_folder_mon); job->dest_folder_mon = old_dest_mon; if( G_UNLIKELY(!ret) ) { /* FIXME: error handling */ } } g_object_unref(inf); } else { if(err) { /* FIXME: error handling */ fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MODERATE); g_error_free(err); err = NULL; /* FM_JOB_RETRY is not supported here */ } else /* EOF is reached */ { ret = TRUE; break; } } } g_file_enumerator_close(enu, NULL, &err); g_object_unref(enu); } else { FmJobErrorAction act = fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MODERATE); g_error_free(err); err = NULL; if(act == FM_JOB_RETRY) goto _retry_enum_children; } } job->skip_dir_content = FALSE; } break; case G_FILE_TYPE_SPECIAL: /* only handle FIFO for local files */ if(g_file_is_native(src) && g_file_is_native(dest)) { char* src_path = g_file_get_path(src); struct stat src_st; int r; r = lstat(src_path, &src_st); g_free(src_path); if(r == 0) { /* Handle FIFO on native file systems. */ if(S_ISFIFO(src_st.st_mode)) { char* dest_path = g_file_get_path(dest); int r = mkfifo(dest_path, src_st.st_mode); g_free(dest_path); if( r == 0) break; else { /* FIXME: error handling */ } } /* FIXME: how about blcok device, char device, and socket? */ } else { /* FIXME: error handling */ } } job->finished += size; fm_file_ops_job_emit_percent(job); default: flags = G_FILE_COPY_ALL_METADATA|G_FILE_COPY_NOFOLLOW_SYMLINKS; _retry_copy: if( !g_file_copy(src, dest, flags, fm_job_get_cancellable(fmjob), progress_cb, fmjob, &err) ) { FmFileOpOption opt = 0; flags &= ~G_FILE_COPY_OVERWRITE; /* handle existing files */ if(err->domain == G_IO_ERROR && err->code == G_IO_ERROR_EXISTS) { g_error_free(err); err = NULL; g_object_ref(dest); /*if dest == new_dest, it will become invalid when we unref new_dest */ if(new_dest) { g_object_unref(new_dest); new_dest = NULL; } opt = fm_file_ops_job_ask_rename(job, src, NULL, dest, &new_dest); g_object_unref(dest); switch(opt) { case FM_FILE_OP_RENAME: dest = new_dest; goto _retry_copy; break; case FM_FILE_OP_OVERWRITE: flags |= G_FILE_COPY_OVERWRITE; goto _retry_copy; break; case FM_FILE_OP_CANCEL: fm_job_cancel(FM_JOB(job)); ret = FALSE; break; case FM_FILE_OP_SKIP: ret = TRUE; break; } } if(!opt && err) { FmJobErrorAction act = fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MODERATE); g_error_free(err); err = NULL; if(act == FM_JOB_RETRY) { job->current_file_finished = 0; goto _retry_copy; } ret = FALSE; } } else ret = TRUE; job->finished += size; job->current_file_finished = 0; if( ret && type != G_FILE_TYPE_DIRECTORY ) { if(job->dest_folder_mon) g_file_monitor_emit_event(job->dest_folder_mon, dest, NULL, G_FILE_MONITOR_EVENT_CREATED); } /* update progress */ fm_file_ops_job_emit_percent(job); break; } /* if this is a cross-device move operation, delete source files. */ if( ret && job->type == FM_FILE_OP_MOVE ) ret = _fm_file_ops_job_delete_file(FM_JOB(job), src, inf); /* delete the source file. */ if(new_dest) g_object_unref(new_dest); return ret; } gboolean _fm_file_ops_job_move_file(FmFileOpsJob* job, GFile* src, GFileInfo* inf, GFile* dest) { /* FIXME: prevent moving to self or moving parent dir to child. */ GError* err = NULL; FmJob* fmjob = FM_JOB(job); const char* src_fs_id; gboolean ret = TRUE; GFile* new_dest = NULL; if( G_LIKELY(inf) ) g_object_ref(inf); else { _retry_query_src_info: inf = g_file_query_info(src, query, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, fm_job_get_cancellable(fmjob), &err); if( !inf ) { FmJobErrorAction act = fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MODERATE); g_error_free(err); err = NULL; if(act == FM_JOB_RETRY) goto _retry_query_src_info; return FALSE; } } src_fs_id = g_file_info_get_attribute_string(inf, G_FILE_ATTRIBUTE_ID_FILESYSTEM); /* Check if source and destination are on the same device */ if( job->type == FM_FILE_OP_UNTRASH || g_strcmp0(src_fs_id, job->dest_fs_id) == 0 ) /* same device */ { FmFileOpOption opt = 0; guint64 size; GFileCopyFlags flags = G_FILE_COPY_ALL_METADATA|G_FILE_COPY_NOFOLLOW_SYMLINKS; /* showing currently processed file. */ fm_file_ops_job_emit_cur_file(job, g_file_info_get_display_name(inf)); _retry_move: if( !g_file_move(src, dest, flags, fm_job_get_cancellable(fmjob), progress_cb, job, &err)) { flags &= ~G_FILE_COPY_OVERWRITE; if(err->domain == G_IO_ERROR && err->code == G_IO_ERROR_EXISTS) { g_object_ref(dest); /*if dest == new_dest, it will become invalid when we unref new_dest */ if(new_dest) { g_object_unref(new_dest); new_dest = NULL; } opt = fm_file_ops_job_ask_rename(job, src, NULL, dest, &new_dest); g_object_unref(dest); g_error_free(err); err = NULL; switch(opt) { case FM_FILE_OP_RENAME: dest = new_dest; goto _retry_move; break; case FM_FILE_OP_OVERWRITE: flags |= G_FILE_COPY_OVERWRITE; if(g_file_info_get_file_type(inf) == G_FILE_TYPE_DIRECTORY) /* merge dirs */ { GFileEnumerator* enu = g_file_enumerate_children(src, query, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, fm_job_get_cancellable(job), &err); if(enu) { GFileInfo* child_inf; while(!fm_job_is_cancelled(job)) { child_inf = g_file_enumerator_next_file(enu, fm_job_get_cancellable(job), &err); if(child_inf) { GFile* child = g_file_get_child(src, g_file_info_get_name(child_inf)); GFile* child_dest = g_file_get_child(dest, g_file_info_get_name(child_inf)); _fm_file_ops_job_move_file(job, child, child_inf, child_dest); g_object_unref(child); g_object_unref(child_dest); g_object_unref(child_inf); } else { if(err) /* error */ { FmJobErrorAction act = fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MODERATE); g_error_free(err); err = NULL; } else /* EOF */ { break; } } } g_object_unref(enu); } else { FmJobErrorAction act = fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MODERATE); g_error_free(err); err = NULL; /* if(act == FM_JOB_RETRY) goto _retry_move; */ } /* remove source dir after its content is merged with destination dir */ if(!g_file_delete(src, fm_job_get_cancellable(job), &err)) { FmJobErrorAction act = fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MODERATE); g_error_free(err); err = NULL; /* FIXME: should this be recoverable? */ } } else /* the destination is a file, just overwrite it. */ goto _retry_move; break; case FM_FILE_OP_CANCEL: fm_job_cancel(FM_JOB(job)); ret = FALSE; break; case FM_FILE_OP_SKIP: ret = TRUE; break; } } if(!opt && err) { FmJobErrorAction act = fm_job_emit_error(fmjob, err, FM_JOB_ERROR_MODERATE); g_error_free(err); err = NULL; if(act == FM_JOB_RETRY) goto _retry_move; } } else { if(job->src_folder_mon) g_file_monitor_emit_event(job->src_folder_mon, src, NULL, G_FILE_MONITOR_EVENT_DELETED); if(job->dest_folder_mon) g_file_monitor_emit_event(job->dest_folder_mon, dest, NULL, G_FILE_MONITOR_EVENT_CREATED); } /* size = g_file_info_get_attribute_uint64(inf, G_FILE_ATTRIBUTE_UNIX_BLOCKS); size *= g_file_info_get_attribute_uint32(inf, G_FILE_ATTRIBUTE_UNIX_BLOCK_SIZE); */ size = g_file_info_get_size(inf); job->finished += size; fm_file_ops_job_emit_percent(job); } else /* use copy if they are on different devices */ { /* use copy & delete */ /* source file will be deleted in _fm_file_ops_job_copy_file() */ ret = _fm_file_ops_job_copy_file(job, src, inf, dest); } if(new_dest) g_object_unref(new_dest); g_object_unref(inf); return ret; } void progress_cb(goffset cur, goffset total, FmFileOpsJob* job) { job->current_file_finished = cur; /* update progress */ fm_file_ops_job_emit_percent(job); } gboolean _fm_file_ops_job_copy_run(FmFileOpsJob* job) { gboolean ret = TRUE; GFile *dest_dir; GFileMonitor *dest_mon; GList* l; FmJob* fmjob = FM_JOB(job); /* prepare the job, count total work needed with FmDeepCountJob */ FmDeepCountJob* dc = fm_deep_count_job_new(job->srcs, FM_DC_JOB_DEFAULT); /* let the deep count job share the same cancellable object. */ fm_job_set_cancellable(FM_JOB(dc), fm_job_get_cancellable(fmjob)); /* FIXME: there is no way to cancel the deep count job here. */ fm_job_run_sync(FM_JOB(dc)); job->total = dc->total_size; if(fm_job_is_cancelled(fmjob)) { g_object_unref(dc); return FALSE; } g_object_unref(dc); g_debug("total size to copy: %llu", job->total); dest_dir = fm_path_to_gfile(job->dest); /* get dummy file monitors for non-native filesystems */ if( g_file_is_native(dest_dir) ) dest_mon = NULL; else { dest_mon = fm_monitor_lookup_dummy_monitor(dest_dir); job->dest_folder_mon = dest_mon; } fm_file_ops_job_emit_prepared(job); for(l = fm_list_peek_head_link(job->srcs); !fm_job_is_cancelled(fmjob) && l; l=l->next) { FmPath* path = (FmPath*)l->data; GFile* src = fm_path_to_gfile(path); GFile* dest = g_file_get_child(dest_dir, path->name); if(!_fm_file_ops_job_copy_file(job, src, NULL, dest)) ret = FALSE; g_object_unref(src); g_object_unref(dest); } /* g_debug("finished: %llu, total: %llu", job->finished, job->total); */ fm_file_ops_job_emit_percent(job); g_object_unref(dest_dir); if(dest_mon) { g_object_unref(dest_mon); job->dest_folder_mon = NULL; } return TRUE; } gboolean _fm_file_ops_job_move_run(FmFileOpsJob* job) { GFile *dest_dir; GFileMonitor *dest_mon; GFileInfo* inf; GList* l; GError* err = NULL; FmJob* fmjob = FM_JOB(job); dev_t dest_dev = 0; gboolean ret = TRUE; FmDeepCountJob* dc; /* get information of destination folder */ g_return_val_if_fail(job->dest, FALSE); dest_dir = fm_path_to_gfile(job->dest); _retry_query_dest_info: inf = g_file_query_info(dest_dir, G_FILE_ATTRIBUTE_STANDARD_IS_VIRTUAL"," G_FILE_ATTRIBUTE_UNIX_DEVICE"," G_FILE_ATTRIBUTE_ID_FILESYSTEM"," G_FILE_ATTRIBUTE_UNIX_DEVICE, 0, fm_job_get_cancellable(fmjob), &err); if(inf) { job->dest_fs_id = g_intern_string(g_file_info_get_attribute_string(inf, G_FILE_ATTRIBUTE_ID_FILESYSTEM)); dest_dev = g_file_info_get_attribute_uint32(inf, G_FILE_ATTRIBUTE_UNIX_DEVICE); /* needed by deep count */ g_object_unref(inf); } else { FmJobErrorAction act = fm_job_emit_error(job, err, FM_JOB_ERROR_MODERATE); if(act == FM_JOB_RETRY) goto _retry_query_dest_info; else { g_object_unref(dest_dir); return FALSE; } } /* prepare the job, count total work needed with FmDeepCountJob */ dc = fm_deep_count_job_new(job->srcs, FM_DC_JOB_PREPARE_MOVE); fm_deep_count_job_set_dest(dc, dest_dev, job->dest_fs_id); fm_job_run_sync(FM_JOB(dc)); job->total = dc->total_size; if( fm_job_is_cancelled(FM_JOB(dc)) ) { g_object_unref(dest_dir); g_object_unref(dc); return FALSE; } g_object_unref(dc); g_debug("total size to move: %llu, dest_fs: %s", job->total, job->dest_fs_id); /* get dummy file monitors for non-native filesystems */ if( g_file_is_native(dest_dir) ) dest_mon = NULL; else { dest_mon = fm_monitor_lookup_dummy_monitor(dest_dir); job->dest_folder_mon = dest_mon; } fm_file_ops_job_emit_prepared(job); for(l = fm_list_peek_head_link(job->srcs); !fm_job_is_cancelled(fmjob) && l; l=l->next) { GFileMonitor *src_mon; FmPath* path = (FmPath*)l->data; GFile* src = fm_path_to_gfile(path); GFile* dest = g_file_get_child(dest_dir, path->name); /* get dummy file monitors for non-native filesystems */ if( g_file_is_native(src) ) src_mon = NULL; else { GFile* src_dir = g_file_get_parent(src); if(src_dir) { src_mon = fm_monitor_lookup_dummy_monitor(src_dir); job->src_folder_mon = src_mon; g_object_unref(src_dir); } else job->src_folder_mon = src_mon = NULL; } if(!_fm_file_ops_job_move_file(job, src, NULL, dest)) ret = FALSE; g_object_unref(src); g_object_unref(dest); if(src_mon) { g_object_unref(src_mon); job->src_folder_mon = NULL; } if(!ret) break; } g_object_unref(dest_dir); if(dest_mon) { g_object_unref(dest_mon); job->dest_folder_mon = NULL; } return ret; }