/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*- */ /* gtksourcefilesaver.c * This file is part of GtkSourceView * * Copyright (C) 2005-2007 - Paolo Borelli and Paolo Maggi * Copyright (C) 2007 - Steve Frécinaux * Copyright (C) 2008 - Jesse van den Kieboom * Copyright (C) 2014, 2016 - Sébastien Wilmet * * GtkSourceView is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * GtkSourceView 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #ifdef HAVE_CONFIG_H #include #endif #include "gtksourcefilesaver.h" #include "gtksourcefile.h" #include "gtksourcebufferinputstream.h" #include "gtksourceencoding.h" #include "gtksourcebuffer.h" #include "gtksourcebuffer-private.h" #include "gtksourceview-enumtypes.h" #include "gtksourceview-i18n.h" /** * SECTION:filesaver * @Short_description: Save a GtkSourceBuffer into a file * @Title: GtkSourceFileSaver * @See_also: #GtkSourceFile, #GtkSourceFileLoader * * A #GtkSourceFileSaver object permits to save a #GtkSourceBuffer into a * #GFile. * * A file saver should be used only for one save operation, including errors * handling. If an error occurs, you can reconfigure the saver and relaunch the * operation with gtk_source_file_saver_save_async(). */ /* The code has been written initially in gedit (GeditDocumentSaver). * It uses a GtkSourceBufferInputStream as input, create converter(s) if needed * for the encoding and the compression, and write the contents to a * GOutputStream (the file). */ #if 0 #define DEBUG(x) (x) #else #define DEBUG(x) #endif #define WRITE_CHUNK_SIZE 8192 #define QUERY_ATTRIBUTES G_FILE_ATTRIBUTE_TIME_MODIFIED enum { PROP_0, PROP_BUFFER, PROP_FILE, PROP_LOCATION, PROP_ENCODING, PROP_NEWLINE_TYPE, PROP_COMPRESSION_TYPE, PROP_FLAGS }; struct _GtkSourceFileSaverPrivate { /* Weak ref to the GtkSourceBuffer. A strong ref could create a * reference cycle in an application. For example a subclass of * GtkSourceBuffer can have a strong ref to the FileSaver. */ GtkSourceBuffer *source_buffer; /* Weak ref to the GtkSourceFile. A strong ref could create a reference * cycle in an application. For example a subclass of GtkSourceFile can * have a strong ref to the FileSaver. */ GtkSourceFile *file; GFile *location; const GtkSourceEncoding *encoding; GtkSourceNewlineType newline_type; GtkSourceCompressionType compression_type; GtkSourceFileSaverFlags flags; GTask *task; }; typedef struct _TaskData TaskData; struct _TaskData { /* The output_stream contains the required converter(s) for the encoding * and the compression type. * The two streams cannot be spliced directly, because: * (1) We need to call the progress callback. * (2) Sync methods must be used for the input stream, and async * methods for the output stream. */ GtkSourceBufferInputStream *input_stream; GOutputStream *output_stream; GFileInfo *info; goffset total_size; GFileProgressCallback progress_cb; gpointer progress_cb_data; GDestroyNotify progress_cb_notify; /* This field is used when cancelling the output stream: an error occurs * and is stored in this field, the output stream is cancelled * asynchronously, and then the error is reported to the task. */ GError *error; gssize chunk_bytes_read; gssize chunk_bytes_written; gchar chunk_buffer[WRITE_CHUNK_SIZE]; guint tried_mount : 1; }; G_DEFINE_TYPE_WITH_PRIVATE (GtkSourceFileSaver, gtk_source_file_saver, G_TYPE_OBJECT) static void read_file_chunk (GTask *task); static void write_file_chunk (GTask *task); static void recover_not_mounted (GTask *task); static TaskData * task_data_new (void) { return g_new0 (TaskData, 1); } static void task_data_free (gpointer data) { TaskData *task_data = data; if (task_data == NULL) { return; } g_clear_object (&task_data->input_stream); g_clear_object (&task_data->output_stream); g_clear_object (&task_data->info); g_clear_error (&task_data->error); if (task_data->progress_cb_notify != NULL) { task_data->progress_cb_notify (task_data->progress_cb_data); } g_free (task_data); } static void gtk_source_file_saver_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GtkSourceFileSaver *saver = GTK_SOURCE_FILE_SAVER (object); switch (prop_id) { case PROP_BUFFER: g_assert (saver->priv->source_buffer == NULL); saver->priv->source_buffer = g_value_get_object (value); g_object_add_weak_pointer (G_OBJECT (saver->priv->source_buffer), (gpointer *)&saver->priv->source_buffer); break; case PROP_FILE: g_assert (saver->priv->file == NULL); saver->priv->file = g_value_get_object (value); g_object_add_weak_pointer (G_OBJECT (saver->priv->file), (gpointer *)&saver->priv->file); break; case PROP_LOCATION: g_assert (saver->priv->location == NULL); saver->priv->location = g_value_dup_object (value); break; case PROP_ENCODING: gtk_source_file_saver_set_encoding (saver, g_value_get_boxed (value)); break; case PROP_NEWLINE_TYPE: gtk_source_file_saver_set_newline_type (saver, g_value_get_enum (value)); break; case PROP_COMPRESSION_TYPE: gtk_source_file_saver_set_compression_type (saver, g_value_get_enum (value)); break; case PROP_FLAGS: gtk_source_file_saver_set_flags (saver, g_value_get_flags (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gtk_source_file_saver_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GtkSourceFileSaver *saver = GTK_SOURCE_FILE_SAVER (object); switch (prop_id) { case PROP_BUFFER: g_value_set_object (value, saver->priv->source_buffer); break; case PROP_FILE: g_value_set_object (value, saver->priv->file); break; case PROP_LOCATION: g_value_set_object (value, saver->priv->location); break; case PROP_ENCODING: g_value_set_boxed (value, saver->priv->encoding); break; case PROP_NEWLINE_TYPE: g_value_set_enum (value, saver->priv->newline_type); break; case PROP_COMPRESSION_TYPE: g_value_set_enum (value, saver->priv->compression_type); break; case PROP_FLAGS: g_value_set_flags (value, saver->priv->flags); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gtk_source_file_saver_dispose (GObject *object) { GtkSourceFileSaver *saver = GTK_SOURCE_FILE_SAVER (object); if (saver->priv->source_buffer != NULL) { g_object_remove_weak_pointer (G_OBJECT (saver->priv->source_buffer), (gpointer *)&saver->priv->source_buffer); saver->priv->source_buffer = NULL; } if (saver->priv->file != NULL) { g_object_remove_weak_pointer (G_OBJECT (saver->priv->file), (gpointer *)&saver->priv->file); saver->priv->file = NULL; } g_clear_object (&saver->priv->location); g_clear_object (&saver->priv->task); G_OBJECT_CLASS (gtk_source_file_saver_parent_class)->dispose (object); } static void gtk_source_file_saver_constructed (GObject *object) { GtkSourceFileSaver *saver = GTK_SOURCE_FILE_SAVER (object); if (saver->priv->file != NULL) { const GtkSourceEncoding *encoding; GtkSourceNewlineType newline_type; GtkSourceCompressionType compression_type; encoding = gtk_source_file_get_encoding (saver->priv->file); gtk_source_file_saver_set_encoding (saver, encoding); newline_type = gtk_source_file_get_newline_type (saver->priv->file); gtk_source_file_saver_set_newline_type (saver, newline_type); compression_type = gtk_source_file_get_compression_type (saver->priv->file); gtk_source_file_saver_set_compression_type (saver, compression_type); if (saver->priv->location == NULL) { saver->priv->location = gtk_source_file_get_location (saver->priv->file); if (saver->priv->location != NULL) { g_object_ref (saver->priv->location); } else { g_warning ("GtkSourceFileSaver: the GtkSourceFile's location is NULL. " "Use gtk_source_file_saver_new_with_target()."); } } } G_OBJECT_CLASS (gtk_source_file_saver_parent_class)->constructed (object); } static void gtk_source_file_saver_class_init (GtkSourceFileSaverClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = gtk_source_file_saver_dispose; object_class->set_property = gtk_source_file_saver_set_property; object_class->get_property = gtk_source_file_saver_get_property; object_class->constructed = gtk_source_file_saver_constructed; /** * GtkSourceFileSaver:buffer: * * The #GtkSourceBuffer to save. The #GtkSourceFileSaver object has a * weak reference to the buffer. * * Since: 3.14 */ g_object_class_install_property (object_class, PROP_BUFFER, g_param_spec_object ("buffer", "GtkSourceBuffer", "", GTK_SOURCE_TYPE_BUFFER, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); /** * GtkSourceFileSaver:file: * * The #GtkSourceFile. The #GtkSourceFileSaver object has a weak * reference to the file. * * Since: 3.14 */ g_object_class_install_property (object_class, PROP_FILE, g_param_spec_object ("file", "GtkSourceFile", "", GTK_SOURCE_TYPE_FILE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); /** * GtkSourceFileSaver:location: * * The #GFile where to save the buffer. By default the location is taken * from the #GtkSourceFile at construction time. * * Since: 3.14 */ g_object_class_install_property (object_class, PROP_LOCATION, g_param_spec_object ("location", "Location", "", G_TYPE_FILE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); /** * GtkSourceFileSaver:encoding: * * The file's encoding. * * Since: 3.14 */ g_object_class_install_property (object_class, PROP_ENCODING, g_param_spec_boxed ("encoding", "Encoding", "", GTK_SOURCE_TYPE_ENCODING, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); /** * GtkSourceFileSaver:newline-type: * * The newline type. * * Since: 3.14 */ g_object_class_install_property (object_class, PROP_NEWLINE_TYPE, g_param_spec_enum ("newline-type", "Newline type", "", GTK_SOURCE_TYPE_NEWLINE_TYPE, GTK_SOURCE_NEWLINE_TYPE_LF, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); /** * GtkSourceFileSaver:compression-type: * * The compression type. * * Since: 3.14 */ g_object_class_install_property (object_class, PROP_COMPRESSION_TYPE, g_param_spec_enum ("compression-type", "Compression type", "", GTK_SOURCE_TYPE_COMPRESSION_TYPE, GTK_SOURCE_COMPRESSION_TYPE_NONE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); /** * GtkSourceFileSaver:flags: * * File saving flags. * * Since: 3.14 */ g_object_class_install_property (object_class, PROP_FLAGS, g_param_spec_flags ("flags", "Flags", "", GTK_SOURCE_TYPE_FILE_SAVER_FLAGS, GTK_SOURCE_FILE_SAVER_FLAGS_NONE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); /* Due to potential deadlocks when registering types, we need to * ensure the dependent private class GtkSourceBufferInputStream * has been registered up front. * * See https://bugzilla.gnome.org/show_bug.cgi?id=780216 */ g_type_ensure (GTK_SOURCE_TYPE_BUFFER_INPUT_STREAM); } static void gtk_source_file_saver_init (GtkSourceFileSaver *saver) { saver->priv = gtk_source_file_saver_get_instance_private (saver); } /* BEGIN NOTE: * * This fixes an issue in GOutputStream that applies the atomic replace save * strategy. The stream moves the written file to the original file when the * stream is closed. However, there is no way currently to tell the stream that * the save should be aborted (there could be a conversion error). The patch * explicitly closes the output stream in all these cases with a GCancellable in * the cancelled state, causing the output stream to close, but not move the * file. This makes use of an implementation detail in the local file stream * and should be properly fixed by adding the appropriate API in GIO. Until * then, at least we prevent data corruption for now. * * Relevant bug reports: * * Bug 615110 - write file ignore encoding errors (gedit) * https://bugzilla.gnome.org/show_bug.cgi?id=615110 * * Bug 602412 - g_file_replace does not restore original file when there is * errors while writing (glib/gio) * https://bugzilla.gnome.org/show_bug.cgi?id=602412 */ static void cancel_output_stream_ready_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { GOutputStream *output_stream = G_OUTPUT_STREAM (source_object); GTask *task = G_TASK (user_data); TaskData *task_data; task_data = g_task_get_task_data (task); g_output_stream_close_finish (output_stream, result, NULL); if (task_data->error != NULL) { GError *error = task_data->error; task_data->error = NULL; g_task_return_error (task, error); } else { g_task_return_boolean (task, FALSE); } } static void cancel_output_stream (GTask *task) { TaskData *task_data; GCancellable *cancellable; DEBUG ({ g_print ("Cancel output stream\n"); }); task_data = g_task_get_task_data (task); cancellable = g_cancellable_new (); g_cancellable_cancel (cancellable); g_output_stream_close_async (task_data->output_stream, g_task_get_priority (task), cancellable, cancel_output_stream_ready_cb, task); g_object_unref (cancellable); } /* * END NOTE */ static void query_info_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { GFile *location = G_FILE (source_object); GTask *task = G_TASK (user_data); TaskData *task_data; GError *error = NULL; DEBUG ({ g_print ("Finished query info on file\n"); }); task_data = g_task_get_task_data (task); g_clear_object (&task_data->info); task_data->info = g_file_query_info_finish (location, result, &error); if (error != NULL) { DEBUG ({ g_print ("Query info failed: %s\n", error->message); }); g_task_return_error (task, error); return; } g_task_return_boolean (task, TRUE); } static void close_output_stream_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { GOutputStream *output_stream = G_OUTPUT_STREAM (source_object); GTask *task = G_TASK (user_data); GtkSourceFileSaver *saver; GError *error = NULL; DEBUG ({ g_print ("%s\n", G_STRFUNC); }); saver = g_task_get_source_object (task); g_output_stream_close_finish (output_stream, result, &error); if (error != NULL) { DEBUG ({ g_print ("Closing stream error: %s\n", error->message); }); g_task_return_error (task, error); return; } /* Get the file info: note we cannot use * g_file_output_stream_query_info_async() since it is not able to get * the modification time. */ DEBUG ({ g_print ("Query info on file\n"); }); g_file_query_info_async (saver->priv->location, QUERY_ATTRIBUTES, G_FILE_QUERY_INFO_NONE, g_task_get_priority (task), g_task_get_cancellable (task), query_info_cb, task); } static void write_complete (GTask *task) { TaskData *task_data; GError *error = NULL; DEBUG ({ g_print ("Close input stream\n"); }); task_data = g_task_get_task_data (task); g_input_stream_close (G_INPUT_STREAM (task_data->input_stream), g_task_get_cancellable (task), &error); if (error != NULL) { DEBUG ({ g_print ("Closing input stream error: %s\n", error->message); }); g_clear_error (&task_data->error); task_data->error = error; cancel_output_stream (task); return; } DEBUG ({ g_print ("Close output stream\n"); }); g_output_stream_close_async (task_data->output_stream, g_task_get_priority (task), g_task_get_cancellable (task), close_output_stream_cb, task); } static void write_file_chunk_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { GOutputStream *output_stream = G_OUTPUT_STREAM (source_object); GTask *task = G_TASK (user_data); TaskData *task_data; gssize bytes_written; GError *error = NULL; DEBUG ({ g_print ("%s\n", G_STRFUNC); }); task_data = g_task_get_task_data (task); bytes_written = g_output_stream_write_finish (output_stream, result, &error); DEBUG ({ g_print ("Written: %" G_GSSIZE_FORMAT "\n", bytes_written); }); if (error != NULL) { DEBUG ({ g_print ("Write error: %s\n", error->message); }); g_clear_error (&task_data->error); task_data->error = error; cancel_output_stream (task); return; } task_data->chunk_bytes_written += bytes_written; /* Write again */ if (task_data->chunk_bytes_written < task_data->chunk_bytes_read) { write_file_chunk (task); return; } if (task_data->progress_cb != NULL) { gsize total_chars_written; total_chars_written = _gtk_source_buffer_input_stream_tell (task_data->input_stream); task_data->progress_cb (total_chars_written, task_data->total_size, task_data->progress_cb_data); } read_file_chunk (task); } static void write_file_chunk (GTask *task) { TaskData *task_data; DEBUG ({ g_print ("%s\n", G_STRFUNC); }); task_data = g_task_get_task_data (task); g_output_stream_write_async (task_data->output_stream, task_data->chunk_buffer + task_data->chunk_bytes_written, task_data->chunk_bytes_read - task_data->chunk_bytes_written, g_task_get_priority (task), g_task_get_cancellable (task), write_file_chunk_cb, task); } static void read_file_chunk (GTask *task) { TaskData *task_data; GError *error = NULL; DEBUG ({ g_print ("%s\n", G_STRFUNC); }); task_data = g_task_get_task_data (task); task_data->chunk_bytes_written = 0; /* We use sync methods on doc stream since it is in memory. Using async * would be racy and we could end up with invalid iters. */ task_data->chunk_bytes_read = g_input_stream_read (G_INPUT_STREAM (task_data->input_stream), task_data->chunk_buffer, WRITE_CHUNK_SIZE, g_task_get_cancellable (task), &error); if (error != NULL) { g_clear_error (&task_data->error); task_data->error = error; cancel_output_stream (task); return; } /* Check if we finished reading and writing. */ if (task_data->chunk_bytes_read == 0) { write_complete (task); return; } write_file_chunk (task); } static void replace_file_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { GFile *location = G_FILE (source_object); GTask *task = G_TASK (user_data); GtkSourceFileSaver *saver; TaskData *task_data; GFileOutputStream *file_output_stream; GOutputStream *output_stream; GError *error = NULL; DEBUG ({ g_print ("%s\n", G_STRFUNC); }); saver = g_task_get_source_object (task); task_data = g_task_get_task_data (task); file_output_stream = g_file_replace_finish (location, result, &error); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_MOUNTED) && !task_data->tried_mount) { recover_not_mounted (task); g_error_free (error); return; } else if (error != NULL) { DEBUG ({ g_print ("Opening file failed: %s\n", error->message); }); g_task_return_error (task, error); return; } if (saver->priv->compression_type == GTK_SOURCE_COMPRESSION_TYPE_GZIP) { GZlibCompressor *compressor; DEBUG ({ g_print ("Use gzip compressor\n"); }); compressor = g_zlib_compressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP, -1); output_stream = g_converter_output_stream_new (G_OUTPUT_STREAM (file_output_stream), G_CONVERTER (compressor)); g_object_unref (compressor); g_object_unref (file_output_stream); } else { output_stream = G_OUTPUT_STREAM (file_output_stream); } /* FIXME: manage converter error? */ DEBUG ({ g_print ("Encoding charset: %s\n", gtk_source_encoding_get_charset (saver->priv->encoding)); }); if (saver->priv->encoding != gtk_source_encoding_get_utf8 ()) { GCharsetConverter *converter; converter = g_charset_converter_new (gtk_source_encoding_get_charset (saver->priv->encoding), "UTF-8", NULL); g_clear_object (&task_data->output_stream); task_data->output_stream = g_converter_output_stream_new (output_stream, G_CONVERTER (converter)); g_object_unref (converter); g_object_unref (output_stream); } else { g_clear_object (&task_data->output_stream); task_data->output_stream = G_OUTPUT_STREAM (output_stream); } task_data->total_size = _gtk_source_buffer_input_stream_get_total_size (task_data->input_stream); DEBUG ({ g_print ("Total number of characters: %" G_GINT64_FORMAT "\n", task_data->total_size); }); read_file_chunk (task); } static void begin_write (GTask *task) { GtkSourceFileSaver *saver; gboolean create_backup; saver = g_task_get_source_object (task); create_backup = (saver->priv->flags & GTK_SOURCE_FILE_SAVER_FLAGS_CREATE_BACKUP) != 0; DEBUG ({ g_print ("Start replacing file contents\n"); g_print ("Make backup: %s\n", create_backup ? "yes" : "no"); }); g_file_replace_async (saver->priv->location, NULL, create_backup, G_FILE_CREATE_NONE, g_task_get_priority (task), g_task_get_cancellable (task), replace_file_cb, task); } static void check_externally_modified_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { GFile *location = G_FILE (source_object); GTask *task = G_TASK (user_data); GtkSourceFileSaver *saver; TaskData *task_data; GFileInfo *info; GTimeVal old_mtime; GTimeVal cur_mtime; GError *error = NULL; DEBUG ({ g_print ("%s\n", G_STRFUNC); }); saver = g_task_get_source_object (task); task_data = g_task_get_task_data (task); info = g_file_query_info_finish (location, result, &error); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_MOUNTED) && !task_data->tried_mount) { recover_not_mounted (task); g_error_free (error); return; } /* It's perfectly fine if the file doesn't exist yet. */ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_clear_error (&error); } else if (error != NULL) { DEBUG ({ g_print ("Check externally modified failed: %s\n", error->message); }); g_task_return_error (task, error); return; } if (_gtk_source_file_get_modification_time (saver->priv->file, &old_mtime) && info != NULL && g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_TIME_MODIFIED)) { g_file_info_get_modification_time (info, &cur_mtime); if (old_mtime.tv_sec != cur_mtime.tv_sec || old_mtime.tv_usec != cur_mtime.tv_usec) { DEBUG ({ g_print ("The file is externally modified\n"); }); g_task_return_new_error (task, GTK_SOURCE_FILE_SAVER_ERROR, GTK_SOURCE_FILE_SAVER_ERROR_EXTERNALLY_MODIFIED, _("The file is externally modified.")); g_object_unref (info); return; } } begin_write (task); if (info != NULL) { g_object_unref (info); } } static void check_externally_modified (GTask *task) { GtkSourceFileSaver *saver; gboolean save_as = FALSE; saver = g_task_get_source_object (task); if (saver->priv->file != NULL) { GFile *prev_location; prev_location = gtk_source_file_get_location (saver->priv->file); /* Don't check for externally modified for a "save as" operation, * because the user has normally accepted to overwrite the file if it * already exists. */ save_as = (prev_location == NULL || !g_file_equal (prev_location, saver->priv->location)); } if (saver->priv->flags & GTK_SOURCE_FILE_SAVER_FLAGS_IGNORE_MODIFICATION_TIME || save_as) { begin_write (task); return; } DEBUG ({ g_print ("Check externally modified\n"); }); g_file_query_info_async (saver->priv->location, G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_QUERY_INFO_NONE, g_task_get_priority (task), g_task_get_cancellable (task), check_externally_modified_cb, task); } static void mount_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { GFile *location = G_FILE (source_object); GTask *task = G_TASK (user_data); GError *error = NULL; DEBUG ({ g_print ("%s\n", G_STRFUNC); }); g_file_mount_enclosing_volume_finish (location, result, &error); if (error != NULL) { g_task_return_error (task, error); return; } check_externally_modified (task); } static void recover_not_mounted (GTask *task) { GtkSourceFileSaver *saver; TaskData *task_data; GMountOperation *mount_operation; saver = g_task_get_source_object (task); task_data = g_task_get_task_data (task); mount_operation = _gtk_source_file_create_mount_operation (saver->priv->file); DEBUG ({ g_print ("%s\n", G_STRFUNC); }); task_data->tried_mount = TRUE; g_file_mount_enclosing_volume (saver->priv->location, G_MOUNT_MOUNT_NONE, mount_operation, g_task_get_cancellable (task), mount_cb, task); g_object_unref (mount_operation); } GQuark gtk_source_file_saver_error_quark (void) { static GQuark quark = 0; if (G_UNLIKELY (quark == 0)) { quark = g_quark_from_static_string ("gtk-source-file-saver-error"); } return quark; } /** * gtk_source_file_saver_new: * @buffer: the #GtkSourceBuffer to save. * @file: the #GtkSourceFile. * * Creates a new #GtkSourceFileSaver object. The @buffer will be saved to the * #GtkSourceFile's location. * * This constructor is suitable for a simple "save" operation, when the @file * already contains a non-%NULL #GtkSourceFile:location. * * Returns: a new #GtkSourceFileSaver object. * Since: 3.14 */ GtkSourceFileSaver * gtk_source_file_saver_new (GtkSourceBuffer *buffer, GtkSourceFile *file) { g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), NULL); g_return_val_if_fail (GTK_SOURCE_IS_FILE (file), NULL); return g_object_new (GTK_SOURCE_TYPE_FILE_SAVER, "buffer", buffer, "file", file, NULL); } /** * gtk_source_file_saver_new_with_target: * @buffer: the #GtkSourceBuffer to save. * @file: the #GtkSourceFile. * @target_location: the #GFile where to save the buffer to. * * Creates a new #GtkSourceFileSaver object with a target location. When the * file saving is finished successfully, @target_location is set to the @file's * #GtkSourceFile:location property. If an error occurs, the previous valid * location is still available in #GtkSourceFile. * * This constructor is suitable for a "save as" operation, or for saving a new * buffer for the first time. * * Returns: a new #GtkSourceFileSaver object. * Since: 3.14 */ GtkSourceFileSaver * gtk_source_file_saver_new_with_target (GtkSourceBuffer *buffer, GtkSourceFile *file, GFile *target_location) { g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), NULL); g_return_val_if_fail (GTK_SOURCE_IS_FILE (file), NULL); g_return_val_if_fail (G_IS_FILE (target_location), NULL); return g_object_new (GTK_SOURCE_TYPE_FILE_SAVER, "buffer", buffer, "file", file, "location", target_location, NULL); } /** * gtk_source_file_saver_get_buffer: * @saver: a #GtkSourceFileSaver. * * Returns: (transfer none): the #GtkSourceBuffer to save. * Since: 3.14 */ GtkSourceBuffer * gtk_source_file_saver_get_buffer (GtkSourceFileSaver *saver) { g_return_val_if_fail (GTK_SOURCE_IS_FILE_SAVER (saver), NULL); return saver->priv->source_buffer; } /** * gtk_source_file_saver_get_file: * @saver: a #GtkSourceFileSaver. * * Returns: (transfer none): the #GtkSourceFile. * Since: 3.14 */ GtkSourceFile * gtk_source_file_saver_get_file (GtkSourceFileSaver *saver) { g_return_val_if_fail (GTK_SOURCE_IS_FILE_SAVER (saver), NULL); return saver->priv->file; } /** * gtk_source_file_saver_get_location: * @saver: a #GtkSourceFileSaver. * * Returns: (transfer none): the #GFile where to save the buffer to. * Since: 3.14 */ GFile * gtk_source_file_saver_get_location (GtkSourceFileSaver *saver) { g_return_val_if_fail (GTK_SOURCE_IS_FILE_SAVER (saver), NULL); return saver->priv->location; } /** * gtk_source_file_saver_set_encoding: * @saver: a #GtkSourceFileSaver. * @encoding: (nullable): the new encoding, or %NULL for UTF-8. * * Sets the encoding. If @encoding is %NULL, the UTF-8 encoding will be set. * By default the encoding is taken from the #GtkSourceFile. * * Since: 3.14 */ void gtk_source_file_saver_set_encoding (GtkSourceFileSaver *saver, const GtkSourceEncoding *encoding) { g_return_if_fail (GTK_SOURCE_IS_FILE_SAVER (saver)); g_return_if_fail (saver->priv->task == NULL); if (encoding == NULL) { encoding = gtk_source_encoding_get_utf8 (); } if (saver->priv->encoding != encoding) { saver->priv->encoding = encoding; g_object_notify (G_OBJECT (saver), "encoding"); } } /** * gtk_source_file_saver_get_encoding: * @saver: a #GtkSourceFileSaver. * * Returns: the encoding. * Since: 3.14 */ const GtkSourceEncoding * gtk_source_file_saver_get_encoding (GtkSourceFileSaver *saver) { g_return_val_if_fail (GTK_SOURCE_IS_FILE_SAVER (saver), NULL); return saver->priv->encoding; } /** * gtk_source_file_saver_set_newline_type: * @saver: a #GtkSourceFileSaver. * @newline_type: the new newline type. * * Sets the newline type. By default the newline type is taken from the * #GtkSourceFile. * * Since: 3.14 */ void gtk_source_file_saver_set_newline_type (GtkSourceFileSaver *saver, GtkSourceNewlineType newline_type) { g_return_if_fail (GTK_SOURCE_IS_FILE_SAVER (saver)); g_return_if_fail (saver->priv->task == NULL); if (saver->priv->newline_type != newline_type) { saver->priv->newline_type = newline_type; g_object_notify (G_OBJECT (saver), "newline-type"); } } /** * gtk_source_file_saver_get_newline_type: * @saver: a #GtkSourceFileSaver. * * Returns: the newline type. * Since: 3.14 */ GtkSourceNewlineType gtk_source_file_saver_get_newline_type (GtkSourceFileSaver *saver) { g_return_val_if_fail (GTK_SOURCE_IS_FILE_SAVER (saver), GTK_SOURCE_NEWLINE_TYPE_DEFAULT); return saver->priv->newline_type; } /** * gtk_source_file_saver_set_compression_type: * @saver: a #GtkSourceFileSaver. * @compression_type: the new compression type. * * Sets the compression type. By default the compression type is taken from the * #GtkSourceFile. * * Since: 3.14 */ void gtk_source_file_saver_set_compression_type (GtkSourceFileSaver *saver, GtkSourceCompressionType compression_type) { g_return_if_fail (GTK_SOURCE_IS_FILE_SAVER (saver)); g_return_if_fail (saver->priv->task == NULL); if (saver->priv->compression_type != compression_type) { saver->priv->compression_type = compression_type; g_object_notify (G_OBJECT (saver), "compression-type"); } } /** * gtk_source_file_saver_get_compression_type: * @saver: a #GtkSourceFileSaver. * * Returns: the compression type. * Since: 3.14 */ GtkSourceCompressionType gtk_source_file_saver_get_compression_type (GtkSourceFileSaver *saver) { g_return_val_if_fail (GTK_SOURCE_IS_FILE_SAVER (saver), GTK_SOURCE_COMPRESSION_TYPE_NONE); return saver->priv->compression_type; } /** * gtk_source_file_saver_set_flags: * @saver: a #GtkSourceFileSaver. * @flags: the new flags. * * Since: 3.14 */ void gtk_source_file_saver_set_flags (GtkSourceFileSaver *saver, GtkSourceFileSaverFlags flags) { g_return_if_fail (GTK_SOURCE_IS_FILE_SAVER (saver)); g_return_if_fail (saver->priv->task == NULL); if (saver->priv->flags != flags) { saver->priv->flags = flags; g_object_notify (G_OBJECT (saver), "flags"); } } /** * gtk_source_file_saver_get_flags: * @saver: a #GtkSourceFileSaver. * * Returns: the flags. * Since: 3.14 */ GtkSourceFileSaverFlags gtk_source_file_saver_get_flags (GtkSourceFileSaver *saver) { g_return_val_if_fail (GTK_SOURCE_IS_FILE_SAVER (saver), GTK_SOURCE_FILE_SAVER_FLAGS_NONE); return saver->priv->flags; } /** * gtk_source_file_saver_save_async: * @saver: a #GtkSourceFileSaver. * @io_priority: the I/O priority of the request. E.g. %G_PRIORITY_LOW, * %G_PRIORITY_DEFAULT or %G_PRIORITY_HIGH. * @cancellable: (nullable): optional #GCancellable object, %NULL to ignore. * @progress_callback: (scope notified) (nullable): function to call back with * progress information, or %NULL if progress information is not needed. * @progress_callback_data: (closure): user data to pass to @progress_callback. * @progress_callback_notify: (nullable): function to call on * @progress_callback_data when the @progress_callback is no longer needed, or * %NULL. * @callback: (scope async): a #GAsyncReadyCallback to call when the request is * satisfied. * @user_data: user data to pass to @callback. * * Saves asynchronously the buffer into the file. See the #GAsyncResult * documentation to know how to use this function. * * Since: 3.14 */ /* The GDestroyNotify is needed, currently the following bug is not fixed: * https://bugzilla.gnome.org/show_bug.cgi?id=616044 */ void gtk_source_file_saver_save_async (GtkSourceFileSaver *saver, gint io_priority, GCancellable *cancellable, GFileProgressCallback progress_callback, gpointer progress_callback_data, GDestroyNotify progress_callback_notify, GAsyncReadyCallback callback, gpointer user_data) { TaskData *task_data; gboolean check_invalid_chars; gboolean implicit_trailing_newline; g_return_if_fail (GTK_SOURCE_IS_FILE_SAVER (saver)); g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); g_return_if_fail (saver->priv->task == NULL); saver->priv->task = g_task_new (saver, cancellable, callback, user_data); g_task_set_priority (saver->priv->task, io_priority); task_data = task_data_new (); g_task_set_task_data (saver->priv->task, task_data, task_data_free); task_data->progress_cb = progress_callback; task_data->progress_cb_data = progress_callback_data; task_data->progress_cb_notify = progress_callback_notify; if (saver->priv->source_buffer == NULL || saver->priv->file == NULL || saver->priv->location == NULL) { g_task_return_boolean (saver->priv->task, FALSE); return; } check_invalid_chars = (saver->priv->flags & GTK_SOURCE_FILE_SAVER_FLAGS_IGNORE_INVALID_CHARS) == 0; if (check_invalid_chars && _gtk_source_buffer_has_invalid_chars (saver->priv->source_buffer)) { g_task_return_new_error (saver->priv->task, GTK_SOURCE_FILE_SAVER_ERROR, GTK_SOURCE_FILE_SAVER_ERROR_INVALID_CHARS, _("The buffer contains invalid characters.")); return; } DEBUG ({ g_print ("Start saving\n"); }); implicit_trailing_newline = gtk_source_buffer_get_implicit_trailing_newline (saver->priv->source_buffer); /* The BufferInputStream has a strong reference to the buffer. * We create the BufferInputStream here so we are sure that the * buffer will not be destroyed during the file saving. */ task_data->input_stream = _gtk_source_buffer_input_stream_new (GTK_TEXT_BUFFER (saver->priv->source_buffer), saver->priv->newline_type, implicit_trailing_newline); check_externally_modified (saver->priv->task); } /** * gtk_source_file_saver_save_finish: * @saver: a #GtkSourceFileSaver. * @result: a #GAsyncResult. * @error: a #GError, or %NULL. * * Finishes a file saving started with gtk_source_file_saver_save_async(). * * If the file has been saved successfully, the following #GtkSourceFile * properties will be updated: the location, the encoding, the newline type and * the compression type. * * Since the 3.20 version, gtk_text_buffer_set_modified() is called with %FALSE * if the file has been saved successfully. * * Returns: whether the file was saved successfully. * Since: 3.14 */ gboolean gtk_source_file_saver_save_finish (GtkSourceFileSaver *saver, GAsyncResult *result, GError **error) { gboolean ok; g_return_val_if_fail (GTK_SOURCE_IS_FILE_SAVER (saver), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); g_return_val_if_fail (g_task_is_valid (result, saver), FALSE); ok = g_task_propagate_boolean (G_TASK (result), error); if (ok && saver->priv->file != NULL) { TaskData *task_data; gtk_source_file_set_location (saver->priv->file, saver->priv->location); _gtk_source_file_set_encoding (saver->priv->file, saver->priv->encoding); _gtk_source_file_set_newline_type (saver->priv->file, saver->priv->newline_type); _gtk_source_file_set_compression_type (saver->priv->file, saver->priv->compression_type); _gtk_source_file_set_externally_modified (saver->priv->file, FALSE); _gtk_source_file_set_deleted (saver->priv->file, FALSE); _gtk_source_file_set_readonly (saver->priv->file, FALSE); task_data = g_task_get_task_data (G_TASK (result)); if (g_file_info_has_attribute (task_data->info, G_FILE_ATTRIBUTE_TIME_MODIFIED)) { GTimeVal modification_time; g_file_info_get_modification_time (task_data->info, &modification_time); _gtk_source_file_set_modification_time (saver->priv->file, modification_time); } } if (ok && saver->priv->source_buffer != NULL) { gtk_text_buffer_set_modified (GTK_TEXT_BUFFER (saver->priv->source_buffer), FALSE); } g_clear_object (&saver->priv->task); return ok; }