summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml5
-rw-r--r--INSTALL.md8
-rw-r--r--README.md11
-rw-r--r--nautilus/README.md15
-rw-r--r--nautilus/mat2.py247
5 files changed, 11 insertions, 275 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 97354ba..098e3cb 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -18,7 +18,6 @@ linting:bandit:
18 stage: linting 18 stage: linting
19 script: # TODO: remove B405 and B314 19 script: # TODO: remove B405 and B314
20 - bandit ./mat2 --format txt --skip B101 20 - bandit ./mat2 --format txt --skip B101
21 - bandit -r ./nautilus/ --format txt --skip B101
22 - bandit -r ./libmat2 --format txt --skip B101,B404,B603,B405,B314,B108,B311 21 - bandit -r ./libmat2 --format txt --skip B101,B404,B603,B405,B314,B108,B311
23 22
24linting:codespell: 23linting:codespell:
@@ -35,14 +34,12 @@ linting:pylint:
35 stage: linting 34 stage: linting
36 script: 35 script:
37 - pylint --disable=no-else-return,no-else-raise,no-else-continue,unnecessary-comprehension,raise-missing-from,unsubscriptable-object,use-dict-literal,unspecified-encoding,consider-using-f-string,use-list-literal,too-many-statements --extension-pkg-whitelist=cairo,gi ./libmat2 ./mat2 36 - pylint --disable=no-else-return,no-else-raise,no-else-continue,unnecessary-comprehension,raise-missing-from,unsubscriptable-object,use-dict-literal,unspecified-encoding,consider-using-f-string,use-list-literal,too-many-statements --extension-pkg-whitelist=cairo,gi ./libmat2 ./mat2
38 # Once nautilus-python is in Debian, decomment it form the line below
39 - pylint --disable=no-else-return,no-else-raise,no-else-continue,unnecessary-comprehension,raise-missing-from,unsubscriptable-object,use-list-literal --extension-pkg-whitelist=Nautilus,GObject,Gtk,Gio,GLib,gi ./nautilus/mat2.py
40 37
41linting:mypy: 38linting:mypy:
42 image: $CONTAINER_REGISTRY:linting 39 image: $CONTAINER_REGISTRY:linting
43 stage: linting 40 stage: linting
44 script: 41 script:
45 - mypy --ignore-missing-imports mat2 libmat2/*.py ./nautilus/mat2.py 42 - mypy --ignore-missing-imports mat2 libmat2/*.py
46 43
47tests:archlinux: 44tests:archlinux:
48 image: $CONTAINER_REGISTRY:archlinux 45 image: $CONTAINER_REGISTRY:archlinux
diff --git a/INSTALL.md b/INSTALL.md
index b684ae8..7d24d65 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -34,20 +34,16 @@ apt install mat2
34Thanks to [atenart](https://ack.tf/), there is a package available on 34Thanks to [atenart](https://ack.tf/), there is a package available on
35[Fedora's copr]( https://copr.fedorainfracloud.org/coprs/atenart/mat2/ ). 35[Fedora's copr]( https://copr.fedorainfracloud.org/coprs/atenart/mat2/ ).
36 36
37We use copr (cool other packages repo) as the mat2 Nautilus plugin depends on
38python3-nautilus, which isn't available yet in Fedora (but is distributed
39through this copr).
40
41First you need to enable mat2's copr: 37First you need to enable mat2's copr:
42 38
43``` 39```
44dnf -y copr enable atenart/mat2 40dnf -y copr enable atenart/mat2
45``` 41```
46 42
47Then you can install both the mat2 command and Nautilus extension: 43Then you can install mat2:
48 44
49``` 45```
50dnf -y install mat2 mat2-nautilus 46dnf -y install mat2
51``` 47```
52 48
53## Gentoo 49## Gentoo
diff --git a/README.md b/README.md
index 351f9d8..ab2de21 100644
--- a/README.md
+++ b/README.md
@@ -22,9 +22,14 @@ Maybe you don't want to disclose those information.
22This is precisely the job of mat2: getting rid, as much as possible, of 22This is precisely the job of mat2: getting rid, as much as possible, of
23metadata. 23metadata.
24 24
25mat2 provides a command line tool, and graphical user interfaces via a service 25mat2 provides:
26menu for Dolphin, the default file manager of KDE, and an extension for 26- a library called `libmat2`;
27Nautilus, the default file manager of GNOME. 27- a command line tool called `mat2`,
28- a service menu for Dolphin, KDE's default file manager
29
30If you prefer a regular graphical user interface, you might be interested in
31[Metadata Cleaner](https://metadatacleaner.romainvigier.fr/), which is using
32`mat2` under the hood.
28 33
29# Requirements 34# Requirements
30 35
diff --git a/nautilus/README.md b/nautilus/README.md
deleted file mode 100644
index 7450204..0000000
--- a/nautilus/README.md
+++ /dev/null
@@ -1,15 +0,0 @@
1# mat2's Nautilus extension
2
3# Dependencies
4
5- Nautilus (now known as [Files](https://wiki.gnome.org/action/show/Apps/Files))
6- [nautilus-python](https://gitlab.gnome.org/GNOME/nautilus-python) >= 2.10
7
8# Installation
9
10Simply copy the `mat2.py` file to `~/.local/share/nautilus-python/extensions`,
11and launch Nautilus; you should now have a "Remove metadata" item in the
12right-click menu on supported files.
13
14Please note: This is not needed if using a distribution provided package. It
15only applies if installing from source.
diff --git a/nautilus/mat2.py b/nautilus/mat2.py
deleted file mode 100644
index 7e94cac..0000000
--- a/nautilus/mat2.py
+++ /dev/null
@@ -1,247 +0,0 @@
1#!/usr/bin/env python3
2
3"""
4Because writing GUI is non-trivial (cf. https://0xacab.org/jvoisin/mat2/issues/3),
5we decided to write a Nautilus extension instead
6(cf. https://0xacab.org/jvoisin/mat2/issues/2).
7
8The code is a little bit convoluted because Gtk isn't thread-safe,
9so we're not allowed to call anything Gtk-related outside of the main
10thread, so we'll have to resort to using a `queue` to pass "messages" around.
11"""
12
13# pylint: disable=no-name-in-module,unused-argument,import-error
14
15import queue
16import threading
17from typing import Tuple, Optional, List
18from urllib.parse import unquote
19import gettext
20
21import gi
22gi.require_version('Nautilus', '3.0')
23gi.require_version('Gtk', '3.0')
24gi.require_version('GdkPixbuf', '2.0')
25from gi.repository import Nautilus, GObject, Gtk, Gio, GLib, GdkPixbuf
26
27from libmat2 import parser_factory
28
29_ = gettext.gettext
30
31
32def _remove_metadata(fpath) -> Tuple[bool, Optional[str]]:
33 """ This is a simple wrapper around libmat2, because it's
34 easier and cleaner this way.
35 """
36 parser, mtype = parser_factory.get_parser(fpath)
37 if parser is None:
38 return False, mtype
39 return parser.remove_all(), mtype
40
41class Mat2Extension(GObject.GObject, Nautilus.MenuProvider, Nautilus.LocationWidgetProvider):
42 """ This class adds an item to the right-click menu in Nautilus. """
43
44 def __init__(self):
45 super().__init__()
46 self.infobar_hbox = None
47 self.infobar = None
48 self.failed_items = list()
49
50 def __infobar_failure(self):
51 """ Add an hbox to the `infobar` warning about the fact that we didn't
52 manage to remove the metadata from every single file.
53 """
54 self.infobar.set_show_close_button(True)
55 self.infobar_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
56
57 btn = Gtk.Button(_("Show"))
58 btn.connect("clicked", self.__cb_show_failed)
59 self.infobar_hbox.pack_end(btn, False, False, 0)
60
61 infobar_msg = Gtk.Label(_("Failed to clean some items"))
62 self.infobar_hbox.pack_start(infobar_msg, False, False, 0)
63
64 self.infobar.get_content_area().pack_start(self.infobar_hbox, True, True, 0)
65 self.infobar.show_all()
66
67 def get_widget(self, uri, window) -> Gtk.Widget:
68 """ This is the method that we have to implement (because we're
69 a LocationWidgetProvider) in order to show our infobar.
70 """
71 self.infobar = Gtk.InfoBar()
72 self.infobar.set_message_type(Gtk.MessageType.ERROR)
73 self.infobar.connect("response", self.__cb_infobar_response)
74
75 return self.infobar
76
77 def __cb_infobar_response(self, infobar, response):
78 """ Callback for the infobar close button.
79 """
80 if response == Gtk.ResponseType.CLOSE:
81 self.infobar_hbox.destroy()
82 self.infobar.hide()
83
84 def __cb_show_failed(self, button):
85 """ Callback to show a popup containing a list of files
86 that we didn't manage to clean.
87 """
88
89 # FIXME this should be done only once the window is destroyed
90 self.infobar_hbox.destroy()
91 self.infobar.hide()
92
93 window = Gtk.Window()
94 headerbar = Gtk.HeaderBar()
95 window.set_titlebar(headerbar)
96 headerbar.props.title = _("Metadata removal failed")
97
98 close_buton = Gtk.Button(_("Close"))
99 close_buton.connect("clicked", lambda _: window.close())
100 headerbar.pack_end(close_buton)
101
102 box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
103 window.add(box)
104
105 box.add(self.__create_treeview())
106 window.show_all()
107
108 @staticmethod
109 def __validate(fileinfo) -> Tuple[bool, str]:
110 """ Validate if a given file FileInfo `fileinfo` can be processed.
111 Returns a boolean, and a textreason why"""
112 if fileinfo.get_uri_scheme() != "file" or fileinfo.is_directory():
113 return False, _("Not a file")
114 elif not fileinfo.can_write():
115 return False, _("Not writeable")
116 return True, ""
117
118 def __create_treeview(self) -> Gtk.TreeView:
119 liststore = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
120 treeview = Gtk.TreeView(model=liststore)
121
122 renderer_pixbuf = Gtk.CellRendererPixbuf()
123 column_pixbuf = Gtk.TreeViewColumn("Icon", renderer_pixbuf, pixbuf=0)
124 treeview.append_column(column_pixbuf)
125
126 for idx, name in enumerate([_('File'), _('Reason')]):
127 renderer_text = Gtk.CellRendererText()
128 column_text = Gtk.TreeViewColumn(name, renderer_text, text=idx+1)
129 treeview.append_column(column_text)
130
131 for (fname, mtype, reason) in self.failed_items:
132 # This part is all about adding mimetype icons to the liststore
133 icon = Gio.content_type_get_icon('text/plain' if not mtype else mtype)
134 # in case we don't have the corresponding icon,
135 # we're adding `text/plain`, because we have this one for sureā„¢
136 names = icon.get_names() + ['text/plain', ]
137 icon_theme = Gtk.IconTheme.get_default()
138 for name in names:
139 try:
140 img = icon_theme.load_icon(name, Gtk.IconSize.BUTTON, 0)
141 break
142 except GLib.GError:
143 pass
144
145 liststore.append([img, fname, reason])
146
147 treeview.show_all()
148 return treeview
149
150 def __create_progressbar(self) -> Gtk.ProgressBar:
151 """ Create the progressbar used to notify that files are currently
152 being processed.
153 """
154 self.infobar.set_show_close_button(False)
155 self.infobar.set_message_type(Gtk.MessageType.INFO)
156 self.infobar_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
157
158 progressbar = Gtk.ProgressBar()
159 self.infobar_hbox.pack_start(progressbar, True, True, 0)
160 progressbar.set_show_text(True)
161
162 self.infobar.get_content_area().pack_start(self.infobar_hbox, True, True, 0)
163 self.infobar.show_all()
164
165 return progressbar
166
167 def __update_progressbar(self, processing_queue, progressbar) -> bool:
168 """ This method is run via `Glib.add_idle` to update the progressbar."""
169 try:
170 fname = processing_queue.get(block=False)
171 except queue.Empty:
172 return True
173
174 # `None` is the marker put in the queue to signal that every selected
175 # file was processed.
176 if fname is None:
177 self.infobar_hbox.destroy()
178 self.infobar.hide()
179 if self.failed_items:
180 self.__infobar_failure()
181 if not processing_queue.empty():
182 print("Something went wrong, the queue isn't empty :/")
183 return False
184
185 progressbar.pulse()
186 progressbar.set_text(_("Cleaning %s") % fname)
187 progressbar.show_all()
188 self.infobar_hbox.show_all()
189 self.infobar.show_all()
190 return True
191
192 def __clean_files(self, files: list, processing_queue: queue.Queue) -> bool:
193 """ This method is threaded in order to avoid blocking the GUI
194 while cleaning up the files.
195 """
196 for fileinfo in files:
197 fname = fileinfo.get_name()
198 processing_queue.put(fname)
199
200 valid, reason = self.__validate(fileinfo)
201 if not valid:
202 self.failed_items.append((fname, None, reason))
203 continue
204
205 fpath = unquote(fileinfo.get_uri()[7:]) # `len('file://') = 7`
206 success, mtype = _remove_metadata(fpath)
207 if not success:
208 self.failed_items.append((fname, mtype, _('Unsupported/invalid')))
209 processing_queue.put(None) # signal that we processed all the files
210 return True
211
212 def __cb_menu_activate(self, menu, files):
213 """ This method is called when the user clicked the "clean metadata"
214 menu item.
215 """
216 self.failed_items = list()
217 progressbar = self.__create_progressbar()
218 progressbar.set_pulse_step = 1.0 / len(files)
219 self.infobar.show_all()
220
221 processing_queue = queue.Queue()
222 GLib.idle_add(self.__update_progressbar, processing_queue, progressbar)
223
224 thread = threading.Thread(target=self.__clean_files, args=(files, processing_queue))
225 thread.daemon = True
226 thread.start()
227
228 def get_background_items(self, window, file):
229 """ https://bugzilla.gnome.org/show_bug.cgi?id=784278 """
230 return None
231
232 def get_file_items(self, window, files) -> Optional[List[Nautilus.MenuItem]]:
233 """ This method is the one allowing us to create a menu item.
234 """
235 # Do not show the menu item if not a single file has a chance to be
236 # processed by mat2.
237 if not any((is_valid for (is_valid, _) in map(self.__validate, files))):
238 return None
239
240 item = Nautilus.MenuItem(
241 name="mat2::Remove_metadata",
242 label=_("Remove metadata"),
243 tip=_("Remove metadata")
244 )
245 item.connect('activate', self.__cb_menu_activate, files)
246
247 return [item, ]