-
Notifications
You must be signed in to change notification settings - Fork 30
/
Copy pathtools.py
425 lines (374 loc) · 21.6 KB
/
tools.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from argparse import ArgumentParser
from shutil import copy as fileCopy, which
from os import remove, makedirs, getenv
from os.path import exists as pathExists, join as pathJoin
from subprocess import check_output, call
from re import findall as reFindall, search as reSearch, sub as reSub, M as reM, S as reS
from logging import getLogger, basicConfig
from gettext import translation
APPNAME = 'yandex-disk-indicator'
# Initialize logging
basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s')
LOGGER = getLogger('')
# Setup localization
_ = translation(APPNAME, fallback=True).gettext
# ################### Common utility functions and classes ################### #
def copyFile(src, dst):
# File copy functiion
try:
fileCopy(src, dst)
except:
LOGGER.error("File Copy Error: from %s to %s", src, dst)
def deleteFile(dst):
# Delete file function
try:
remove(dst)
except:
LOGGER.error('File Deletion Error: %s', dst)
def makeDirs(dst):
# Create all child directories up to specified
try:
makedirs(dst, exist_ok=True)
except:
LOGGER.error('Dirs creation Error: %s', dst)
def shortPath(path):
# Make short path to display it
return (path[: 20] + '...' + path[-27:] if len(path) > 50 else path).replace('_', '\u02CD')
class CVal: # Multivalue helper
# Class to work with value that can be None, scalar item or list of items depending of number of items added to it.
def __init__(self, initialValue=None):
self.val = None
self.set(initialValue) # store initial value
self.index = None
def get(self):
# Just returns the current value of cVal
return self.val
def set(self, value):
# Set internal value
self.val = value
if isinstance(self.val, list) and len(self.val) == 1:
self.val = self.val[0]
return self.val
def add(self, item):
# Add item
if isinstance(self.val, list): # Is it third, fourth ... value?
self.val.append(item) # Just append new item to list
elif self.val is None: # Is it first item?
self.val = item # Just store item
else: # It is the second item.
self.val = [self.val, item] # Convert scalar value to list of items.
return self.val
def __iter__(self):
# cVal iterator object initialization
if isinstance(self.val, list): # Is CVal a list?
self.index = -1
elif self.val is None: # Is CVal not defined?
self.index = None
else: # CVal is scalar type.
self.index = -2
return self
def __next__(self):
# cVal iterator support
if self.index is None: # Is CVal not defined?
raise StopIteration # Stop iterations
self.index += 1
if self.index >= 0: # Is CVal a list?
if self.index < len(self.val): # Is there a next element in list?
return self.val[self.index]
self.index = None # There is no more elements in list.
raise StopIteration # Stop iterations
# CVal has scalar type.
self.index = None # Remember that there is no more iterations possible
return self.val
def __bool__(self):
# returns False for empty cVal oterways returns True
return self.val is not None
class Config(dict):
# Configuration is a class to represent stored on disk configuration values
def __init__(self, fileName, load=True,
bools=None, boolval=None,
usequotes=True, delimiter='='):
bools = [['true', 'yes', 'y'], ['false', 'no', 'n']] if bools is None else bools
boolval = ['yes', 'no'] if boolval is None else boolval
super().__init__()
self.fileName = fileName
self.bools = bools # Values to detect boolean in self.load
self.boolval = boolval # Values to write boolean in self.save
self.usequotes = usequotes # Use quotes for keys and values in self.save
self.delimiter = delimiter # Use specified delimiter between key and value
self.changed = False # Change flag (for use outside of the class)
if load:
self.load()
def decode(self, value): # Convert string to value before store it
if value.lower() in self.bools[0]:
value = True
elif value.lower() in self.bools[1]:
value = False
return value
def getValue(self, st): # Find value(s) in string after '='
v = CVal() # Value accumulator
st = st.strip() # Remove starting and ending spaces
if st.startswith(','):
return None # Error: String after '=' starts with comma
while True:
s = reSearch(r'^("[^"]*")|^([^",#]+)', st) # Search for quoted or value without quotes
if s is None:
return None # Error: Nothing found but value expected
start, end = s.span()
vv = st[start: end].strip() # Get found value
if vv.startswith('"'):
vv = vv[1: -1] # Remove quotes
v.add(self.decode(vv)) # Decode and store value
st = st[end:].lstrip() # Remove value and following spaces from string
if st == '':
return v.get() # EOF normaly reached (after last value in string)
if st.startswith(','): # String is continued with comma?
st = st[1:].lstrip() # Remove comma and following spaces
if st != '': # String is continued after comma?
continue # Continue to search values
# else: # Error: No value after comma
# else: # Error: Next symbol is not comma
return None # Error
def load(self):
# Reads config file to dictionary.
# Config file should contain key=value rows.
# Key can be quoted or not.
# Value can be one item or list of comma-separated items. Each value item can be quoted or not.
# When value is a single item then it creates key:value item in dictionary
# When value is a list of items it creates key:[value, value,...] dictionary's item.
try: # Read configuration file into list of tuples ignoring blank
# lines, lines without delimiter, and lines with comments.
with open(self.fileName) as cf:
res = [reFindall(r'^\s*(.+?)\s*%s\s*(.*)$' % self.delimiter, l)[0]
for l in cf if l and self.delimiter in l and l.lstrip()[0] != '#']
self.readSuccess = True
except:
LOGGER.error('Config file read error: %s', self.fileName)
self.readSuccess = False
return False
for kv, vv in res: # Parse each line
# Check key
key = reFindall(r'^"([^"]+)"$|^([\w-]+)$', kv)
if key == []:
LOGGER.warning('Wrong key in line \'%s %s %s\'', kv, self.delimiter, vv)
else: # Key is OK
key = key[0][0] + key[0][1] # Join two possible keys variants (with and without quotes)
if vv.strip() == '':
LOGGER.warning('No value specified in line \'%s %s %s\'', kv, self.delimiter, vv)
else: # Value is not empty
value = self.getValue(vv) # Parse values
if value is None:
LOGGER.warning('Wrong value(s) in line \'%s %s %s\'', kv, self.delimiter, vv)
else: # Value is OK
if key in self.keys(): # Check double values
LOGGER.warning('Double values for one key:\n%s = %s\nand\n%s = %s\nLast one is stored.',
key, self[key], key, value)
self[key] = value # Store last value
LOGGER.debug('Config value read as: %s = %s', key, str(value))
LOGGER.info('Config read: %s', self.fileName)
return True
def encode(self, val): # Convert value to string before save it
if isinstance(val, bool): # Treat Boolean
val = self.boolval[0] if val else self.boolval[1]
if self.usequotes:
val = '"' + val + '"' # Put value within quotes
return val
def save(self):
# save in-memory configuration to file on disk
try: # Read the file in buffer
with open(self.fileName, 'rt') as cf:
buf = cf.read()
except:
LOGGER.warning('Config file access error, a new file (%s) will be created', self.fileName)
buf = ''
buf = reSub(r'[\n]*$', '\n', buf) # Remove all ending blank lines except the one.
for key, value in self.items():
if value is None:
res = '' # Remove 'key=value' from file if value is None
LOGGER.debug('Config value \'%s\' will be removed', key)
else: # Make a line with value
res = self.delimiter.join([key,
', '.join(self.encode(val) for val in CVal(value))]) + '\n'
LOGGER.debug('Config value to save: %s', res[:-1])
# Find line with key in file the buffer
sRe = reSearch(r'^[ \t]*["]?%s["]?[ \t]*%s.+\n' % (key, self.delimiter), buf, flags=reM)
if sRe is not None: # Value has been found
buf = sRe.re.sub(res, buf) # Replace it with new value
elif res != '': # Value was not found and value is not empty
buf += res # Add new value to end of file buffer
try:
with open(self.fileName, 'wt') as cf:
cf.write(buf) # Write updated buffer to file
except:
LOGGER.error('Config file write error: %s', self.fileName)
return False
LOGGER.info('Config written: %s', self.fileName)
self.changed = False # Reset flag of change in not stored config
return True
def activateActions(activate, appInstPath):
# Install/deinstall file extensions
userHome = getenv("HOME")
result = False
try: # Catch all exceptions during FM action activation/deactivation
# --- Actions for Nautilus ---
if which("nautilus") is not None:
LOGGER.info("Nautilus installed")
ver = check_output([which("lsb_release"), "-rs"])
if ver != '' and float(ver) < 12.10:
nautilusPath = ".gnome2/nautilus-scripts/"
else:
nautilusPath = ".local/share/nautilus/scripts"
LOGGER.debug(nautilusPath)
if activate: # Install actions for Nautilus
copyFile(pathJoin(appInstPath, "fm-actions/Nautilus_Nemo/publish"),
pathJoin(userHome, nautilusPath, _("Publish via Yandex.Disk")))
copyFile(pathJoin(appInstPath, "fm-actions/Nautilus_Nemo/unpublish"),
pathJoin(userHome, nautilusPath, _("Unpublish from Yandex.disk")))
else: # Remove actions for Nautilus
deleteFile(pathJoin(userHome, nautilusPath, _("Publish via Yandex.Disk")))
deleteFile(pathJoin(userHome, nautilusPath, _("Unpublish from Yandex.disk")))
result = True
# --- Actions for Nemo ---
if which("nemo") is not None:
LOGGER.info("Nemo installed")
if activate: # Install actions for Nemo
copyFile(pathJoin(appInstPath, "fm-actions/Nautilus_Nemo/publish"),
pathJoin(userHome, ".local/share/nemo/scripts", _("Publish via Yandex.Disk")))
copyFile(pathJoin(appInstPath, "fm-actions/Nautilus_Nemo/unpublish"),
pathJoin(userHome, ".local/share/nemo/scripts", _("Unpublish from Yandex.disk")))
else: # Remove actions for Nemo
deleteFile(pathJoin(userHome, ".gnome2/nemo-scripts", _("Publish via Yandex.Disk")))
deleteFile(pathJoin(userHome, ".gnome2/nemo-scripts", _("Unpublish from Yandex.disk")))
result = True
# --- Actions for Thunar ---
if which("thunar") is not None:
LOGGER.info("Thunar installed")
ucaPath = pathJoin(userHome, ".config/Thunar/uca.xml")
# Read uca.xml
with open(ucaPath) as ucaf:
[(ust, actions, uen)] = reFindall(r'(^.*<actions>)(.*)(<\/actions>)', ucaf.read(), reS)
acts = reFindall(r'(<action>.*?<\/action>)', actions, reS)
nActs = {reFindall(r'<name>(.+?)<\/name>', u, reS)[0]: u for u in acts}
if activate: # Install actions for Thunar
if _("Publish via Yandex.Disk") not in nActs.keys():
nActs[_("Publish via Yandex.Disk")] = ("<action><icon>folder-publicshare</icon>" +
'<name>' + _("Publish via Yandex.Disk") +
'</name><command>yandex-disk publish %f | xclip -filter -selection' +
' clipboard; zenity --info ' +
'--window-icon=/usr/share/yd-tools/icons/yd-128.png ' +
'--title="Yandex.Disk" --ok-label="' + _('Close') + '" --text="' +
_('URL to file: %f was copied into clipboard.') +
'"</command><description/><patterns>*</patterns>' +
'<directories/><audio-files/><image-files/><other-files/>' +
"<text-files/><video-files/></action>")
if _("Unpublish from Yandex.disk") not in nActs.keys():
nActs[_("Unpublish from Yandex.disk")] = ("<action><icon>folder</icon><name>" +
_("Unpublish from Yandex.disk") +
'</name><command>zenity --info ' +
'--window-icon=/usr/share/yd-tools/icons/yd-128_g.png --ok-label="' +
_('Close') + '" --title="Yandex.Disk" --text="' +
_("Unpublish from Yandex.disk") +
': `yandex-disk unpublish %f`"</command>' +
'<description/><patterns>*</patterns>' +
'<directories/><audio-files/><image-files/><other-files/>' +
"<text-files/><video-files/></action>")
else: # Remove actions for Thunar
if _("Publish via Yandex.Disk") in nActs.keys():
del nActs[_("Publish via Yandex.Disk")]
if _("Unpublish from Yandex.disk") in nActs.keys():
del nActs[_("Unpublish from Yandex.disk")]
# Save uca.xml
with open(ucaPath, 'wt') as ucaf:
ucaf.write(ust + ''.join(u for u in nActs.values()) + uen)
result = True
# --- Actions for Dolphin ---
if which("dolphin") is not None:
LOGGER.info("Dolphin installed")
if activate: # Install actions for Dolphin
makeDirs(pathJoin(userHome, '.local/share/kservices5/ServiceMenus'))
copyFile(pathJoin(appInstPath, "fm-actions/Dolphin/ydpublish.desktop"),
pathJoin(userHome, ".local/share/kservices5/ServiceMenus/ydpublish.desktop"))
else: # Remove actions for Dolphin
deleteFile(pathJoin(userHome, ".local/share/kservices5/ServiceMenus/ydpublish.desktop"))
result = True
# --- Actions for Pantheon-files ---
if which("pantheon-files") is not None:
LOGGER.info("Pantheon-files installed")
ctrs_path = "/usr/share/contractor/"
gksudo = which("gksudo")
if activate: # Install actions for Pantheon-files
src_path = pathJoin(appInstPath, "fm-actions", "pantheon-files")
ctr_pub = pathJoin(src_path, "yandex-disk-indicator-publish.contract")
ctr_unpub = pathJoin(src_path, "yandex-disk-indicator-unpublish.contract")
res = call([gksudo, "-D", "yd-tools", "cp", ctr_pub, ctr_unpub, ctrs_path])
if res == 0:
result = True
else:
LOGGER.error("Cannot enable actions for Pantheon-files")
else: # Remove actions for Pantheon-files
res = call([gksudo, "-D", "yd-tools", "rm",
pathJoin(ctrs_path, "yandex-disk-indicator-publish.contract"),
pathJoin(ctrs_path, "yandex-disk-indicator-unpublish.contract")])
if res == 0:
result = True
else:
LOGGER.error("Cannot disable actions for Pantheon-files")
# --- Actions for Caja ---
if which("caja") is not None:
LOGGER.info("Caja installed")
if activate: # Install actions for Nemo
copyFile(pathJoin(appInstPath, "fm-actions/Nautilus_Nemo/publish"),
pathJoin(userHome, ".config/caja/scripts", _("Publish via Yandex.Disk")))
copyFile(pathJoin(appInstPath, "fm-actions/Nautilus_Nemo/unpublish"),
pathJoin(userHome, ".config/caja/scripts", _("Unpublish from Yandex.disk")))
else: # Remove actions for Nemo
deleteFile(pathJoin(userHome, ".config/caja/scripts", _("Publish via Yandex.Disk")))
deleteFile(pathJoin(userHome, ".config/caja/scripts", _("Unpublish from Yandex.disk")))
result = True
except Exception as e:
LOGGER.error("The following error occurred during the FM actions activation:\n %s", str(e))
return result
def checkAutoStart(path):
# Check that auto-start is enabled
if pathExists(path):
i = 1 if getenv('XDG_CURRENT_DESKTOP') in ('Unity', 'Pantheon') else 0
with open(path, 'rt') as f:
attr = reFindall(r'\nHidden=(.+)|\nX-GNOME-Autostart-enabled=(.+)', f.read())
if attr:
if attr[0][i] and attr[0][i] == ('true' if i else 'false'):
return True
else:
return True
return False
def setProcName(newname):
# Sets the executable name
from ctypes import cdll, byref, create_string_buffer
libc = cdll.LoadLibrary('libc.so.6')
buff = create_string_buffer(len(newname) + 1)
buff.value = bytes(newname, 'UTF8')
libc.prctl(15, byref(buff), 0, 0, 0)
def argParse(ver):
# Parse command line arguments
parser = ArgumentParser(description=_('Desktop indicator for yandex-disk daemon'), add_help=False)
group = parser.add_argument_group(_('Options'))
group.add_argument('-l', '--log', type=int, choices=range(10, 60, 10), dest='level', default=30,
help=_('Sets the logging level: ' +
'10 - to show all messages (DEBUG), ' +
'20 - to show all messages except debugging messages (INFO), ' +
'30 - to show all messages except debugging and info messages (WARNING), ' +
'40 - to show only error and critical messages (ERROR), ' +
'50 - to show critical messages only (CRITICAL). Default: 30'))
group.add_argument('-c', '--config', dest='cfg', metavar='path', default='',
help=_('Path to configuration file of YandexDisk daemon. ' +
'This daemon will be added to daemons list' +
' if it is not in the current configuration.' +
'Default: \'\''))
group.add_argument('-r', '--remove', dest='rcfg', metavar='path', default='',
help=_('Path to configuration file of daemon that should be removed' +
' from daemos list. Default: \'\''))
group.add_argument('-h', '--help', action='help', help=_('Show this help message and exit'))
group.add_argument('-v', '--version', action='version', version='%(prog)s v.' + ver,
help=_('Print version and exit'))
return parser.parse_args()