Passed
Push — beta ( 8fb445...6d3439 )
by Dean
02:27
created

FSMigrator.run()   A

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 2
dl 0
loc 5
rs 9.4285
1
from plugin.core.constants import PLUGIN_VERSION_BASE
2
from plugin.core.helpers.variable import all
3
4
from lxml import etree
5
import shutil
6
import os
7
8
9
class FSMigrator(object):
10
    migrations = []
11
12
    @classmethod
13
    def register(cls, migration):
14
        cls.migrations.append(migration())
15
16
    @classmethod
17
    def run(cls):
18
        for migration in cls.migrations:
19
            Log.Debug('Running migration: %s', migration)
20
            migration.run()
21
22
23
class Migration(object):
24
    @property
25
    def code_path(self):
26
        return Core.code_path
27
28
    @property
29
    def lib_path(self):
30
        return os.path.join(self.code_path, '..', 'Libraries')
31
32
    @property
33
    def tests_path(self):
34
        return os.path.join(self.code_path, '..', 'Tests')
35
36
    @property
37
    def plex_path(self):
38
        return os.path.abspath(os.path.join(self.code_path, '..', '..', '..', '..'))
39
40
    @property
41
    def preferences_path(self):
42
        return os.path.join(self.plex_path, 'Plug-in Support', 'Preferences', 'com.plexapp.plugins.trakttv.xml')
43
44
    def get_preferences(self):
45
        if not os.path.exists(self.preferences_path):
46
            Log.Error('Unable to find preferences file at "%s", unable to run migration', self.preferences_path)
47
            return {}
48
49
        data = Core.storage.load(self.preferences_path)
50
        doc = etree.fromstring(data)
51
52
        return dict([(elem.tag, elem.text) for elem in doc])
53
54
    def set_preferences(self, changes):
55
        if not os.path.exists(self.preferences_path):
56
            Log.Error('Unable to find preferences file at "%s", unable to run migration', self.preferences_path)
57
            return False
58
59
        data = Core.storage.load(self.preferences_path)
60
        doc = etree.fromstring(data)
61
62
        for key, value in changes.items():
63
            elem = doc.find(key)
64
65
            # Ensure node exists
66
            if elem is None:
67
                elem = etree.SubElement(doc, key)
68
69
            # Update node value, ensure it is a string
70
            elem.text = str(value)
71
72
            Log.Debug('Updated preference with key "%s" to value %s', key, repr(value))
73
74
        # Write back new preferences
75
        Core.storage.save(self.preferences_path, etree.tostring(doc, pretty_print=True))
76
77
    @staticmethod
78
    def delete_file(path, conditions=None):
79
        if not all([c(path) for c in conditions]):
80
            return False
81
82
        try:
83
            os.remove(path)
84
            return True
85
        except Exception, ex:
86
            Log.Warn('Unable to remove file %r - %s', path, ex, exc_info=True)
87
88
        return False
89
90
    @staticmethod
91
    def delete_directory(path, conditions=None):
92
        if not all([c(path) for c in conditions]):
93
            return False
94
95
        try:
96
            shutil.rmtree(path)
97
            return True
98
        except Exception, ex:
99
            Log.Warn('Unable to remove directory %r - %s', path, ex, exc_info=True)
100
101
        return False
102
103
104
class Clean(Migration):
105
    tasks_code = [
106
        (
107
            'delete_file', [
108
                # /core
109
                'core/action.py',
110
                'core/cache.py',
111
                'core/configuration.py',
112
                'core/environment.py',
113
                'core/eventing.py',
114
                'core/logging_handler.py',
115
                'core/logging_reporter.py',
116
                'core/method_manager.py',
117
                'core/migrator.py',
118
                'core/model.py',
119
                'core/network.py',
120
                'core/numeric.py',
121
                'core/task.py',
122
                'core/trakt.py',
123
                'core/trakt_objects.py',
124
125
                # /interface
126
                'interface/main_menu.py',
127
                'interface/sync_menu.py',
128
129
                # /
130
                'libraries.py',
131
                'sync.py'
132
            ], os.path.isfile
133
        ),
134
        (
135
            'delete_directory', [
136
                'data',
137
                'plex',
138
                'pts',
139
                'sync'
140
            ], os.path.isdir
141
        )
142
    ]
143
144
    tasks_lib = [
145
        (
146
            'delete_file', [
147
                # plugin
148
                'Shared/plugin/core/event.py',
149
                'Shared/plugin/core/io.py',
150
                'Shared/plugin/core/jsonw.py',
151
                'Shared/plugin/modules/base.py',
152
                'Shared/plugin/modules/manager.py',
153
                'Shared/plugin/preferences/options/core/base.py',
154
                'Shared/plugin/sync/modes/core/base.py',
155
                'Shared/plugin/sync/modes/fast_pull.py',
156
                'Shared/plugin/sync/modes/pull.py',
157
                'Shared/plugin/sync/modes/push.py',
158
159
                # native
160
                'FreeBSD/i386/apsw.so',
161
                'FreeBSD/i386/llist.so',
162
163
                'FreeBSD/i386/ucs2/apsw.dependencies',
164
                'FreeBSD/i386/ucs2/apsw.file',
165
                'FreeBSD/i386/ucs2/llist.dependencies',
166
                'FreeBSD/i386/ucs2/llist.file',
167
                'FreeBSD/i386/ucs4/apsw.dependencies',
168
                'FreeBSD/i386/ucs4/apsw.file',
169
                'FreeBSD/i386/ucs4/llist.dependencies',
170
                'FreeBSD/i386/ucs4/llist.file',
171
172
                'FreeBSD/x86_64/ucs2/apsw.dependencies',
173
                'FreeBSD/x86_64/ucs2/apsw.file',
174
                'FreeBSD/x86_64/ucs2/llist.dependencies',
175
                'FreeBSD/x86_64/ucs2/llist.file',
176
                'FreeBSD/x86_64/ucs4/apsw.dependencies',
177
                'FreeBSD/x86_64/ucs4/apsw.file',
178
                'FreeBSD/x86_64/ucs4/llist.dependencies',
179
                'FreeBSD/x86_64/ucs4/llist.file',
180
181
                'Windows/i386/apsw.pyd',
182
                'Windows/i386/llist.pyd',
183
184
                'Linux/i386/apsw.so',
185
                'Linux/i386/llist.so',
186
                'Linux/x86_64/apsw.so',
187
                'Linux/x86_64/llist.so',
188
189
                'Linux/armv6_hf/ucs4/apsw.dependencies',
190
                'Linux/armv6_hf/ucs4/apsw.file',
191
                'Linux/armv6_hf/ucs4/apsw.header',
192
                'Linux/armv6_hf/ucs4/llist.dependencies',
193
                'Linux/armv6_hf/ucs4/llist.file',
194
                'Linux/armv6_hf/ucs4/llist.header',
195
196
                'Linux/armv7_hf/ucs4/apsw.dependencies',
197
                'Linux/armv7_hf/ucs4/apsw.file',
198
                'Linux/armv7_hf/ucs4/apsw.header',
199
                'Linux/armv7_hf/ucs4/llist.dependencies',
200
                'Linux/armv7_hf/ucs4/llist.file',
201
                'Linux/armv7_hf/ucs4/llist.header',
202
203
                'Linux/i386/ucs2/apsw.dependencies',
204
                'Linux/i386/ucs2/apsw.file',
205
                'Linux/i386/ucs2/llist.dependencies',
206
                'Linux/i386/ucs2/llist.file',
207
                'Linux/i386/ucs4/apsw.dependencies',
208
                'Linux/i386/ucs4/apsw.file',
209
                'Linux/i386/ucs4/llist.dependencies',
210
                'Linux/i386/ucs4/llist.file',
211
212
                'Linux/x86_64/ucs2/apsw.dependencies',
213
                'Linux/x86_64/ucs2/apsw.file',
214
                'Linux/x86_64/ucs2/llist.dependencies',
215
                'Linux/x86_64/ucs2/llist.file',
216
                'Linux/x86_64/ucs4/apsw.dependencies',
217
                'Linux/x86_64/ucs4/apsw.file',
218
                'Linux/x86_64/ucs4/llist.dependencies',
219
                'Linux/x86_64/ucs4/llist.file',
220
221
                'MacOSX/i386/ucs2/apsw.dependencies',
222
                'MacOSX/i386/ucs2/apsw.file',
223
                'MacOSX/i386/ucs2/llist.dependencies',
224
                'MacOSX/i386/ucs2/llist.file',
225
                'MacOSX/i386/ucs4/apsw.dependencies',
226
                'MacOSX/i386/ucs4/apsw.file',
227
                'MacOSX/i386/ucs4/llist.dependencies',
228
                'MacOSX/i386/ucs4/llist.file',
229
230
                'MacOSX/x86_64/ucs2/apsw.dependencies',
231
                'MacOSX/x86_64/ucs2/apsw.file',
232
                'MacOSX/x86_64/ucs2/llist.dependencies',
233
                'MacOSX/x86_64/ucs2/llist.file',
234
                'MacOSX/x86_64/ucs4/apsw.dependencies',
235
                'MacOSX/x86_64/ucs4/apsw.file',
236
                'MacOSX/x86_64/ucs4/llist.dependencies',
237
                'MacOSX/x86_64/ucs4/llist.file',
238
239
                'Windows/i386/ucs2/apsw.pyd',
240
                'Windows/i386/ucs2/llist.pyd',
241
242
                # asio
243
                'Shared/asio.py',
244
                'Shared/asio_base.py',
245
                'Shared/asio_posix.py',
246
                'Shared/asio_windows.py',
247
                'Shared/asio_windows_interop.py',
248
249
                # concurrent
250
                'Shared/concurrent/futures/_compat.py',
251
252
                # msgpack
253
                'Shared/msgpack/_packer.pyx',
254
                'Shared/msgpack/_unpacker.pyx',
255
                'Shared/msgpack/pack.h',
256
                'Shared/msgpack/pack_template.h',
257
                'Shared/msgpack/sysdep.h',
258
                'Shared/msgpack/unpack.h',
259
                'Shared/msgpack/unpack_define.h',
260
                'Shared/msgpack/unpack_template.h',
261
262
                # playhouse
263
                'Shared/playhouse/pskel',
264
265
                # plex.py
266
                'Shared/plex/core/compat.py',
267
                'Shared/plex/core/event.py',
268
                'Shared/plex/interfaces/library.py',
269
                'Shared/plex/interfaces/plugin.py',
270
271
                # plex.metadata.py
272
                'Shared/plex_metadata/core/cache.py',
273
274
                # requests
275
                'Shared/requests/packages/urllib3/util.py',
276
                'Shared/requests/packages/README.rst',
277
278
                # trakt.py
279
                'Shared/trakt/core/context.py',
280
                'Shared/trakt/interfaces/base/media.py',
281
                'Shared/trakt/interfaces/account.py',
282
                'Shared/trakt/interfaces/rate.py',
283
                'Shared/trakt/interfaces/sync/base.py',
284
                'Shared/trakt/media_mapper.py',
285
                'Shared/trakt/objects.py',
286
                'Shared/trakt/request.py',
287
288
                # tzlocal
289
                'Shared/tzlocal/tests.py',
290
291
                # websocket
292
                'Shared/websocket.py'
293
            ], os.path.isfile
294
        ),
295
        (
296
            'delete_directory', [
297
                # plugin
298
                'Shared/plugin/core/collections',
299
                'Shared/plugin/data',
300
301
                # native
302
                'MacOSX/universal',
303
304
                # pytz
305
                'Shared/pytz/tests',
306
307
                # shove
308
                'Shared/shove',
309
310
                # stuf
311
                'Shared/stuf',
312
313
                # trakt.py
314
                'Shared/trakt/interfaces/movie',
315
                'Shared/trakt/interfaces/show',
316
                'Shared/trakt/interfaces/user',
317
318
                # tzlocal
319
                'Shared/tzlocal/test_data'
320
            ], os.path.isdir
321
        )
322
    ]
323
324
    tasks_tests = [
325
        (
326
            'delete_file', [
327
            ], os.path.isfile
328
        ),
329
        (
330
            'delete_directory', [
331
                'tests/core/mock',
332
            ], os.path.isdir
333
        )
334
    ]
335
336
    def run(self):
337
        if PLUGIN_VERSION_BASE >= (0, 8):
338
            self.upgrade()
339
340
    def upgrade(self):
341
        self.execute(self.tasks_code, 'upgrade', self.code_path)
342
        self.execute(self.tasks_lib, 'upgrade', self.lib_path)
343
        self.execute(self.tasks_tests, 'upgrade', self.tests_path)
344
345
    def execute(self, tasks, name, base_path):
346
        for action, paths, conditions in tasks:
347
            if type(paths) is not list:
348
                paths = [paths]
349
350
            if type(conditions) is not list:
351
                conditions = [conditions]
352
353
            if not hasattr(self, action):
354
                Log.Error('Unknown migration action "%s"', action)
355
                continue
356
357
            m = getattr(self, action)
358
359
            for path in paths:
360
                path = os.path.join(base_path, path)
361
                path = os.path.abspath(path)
362
363
                # Remove file
364
                if m(path, conditions):
365
                    Log.Info('(%s) %s: "%s"', name, action, path)
366
367
                # Remove .pyc files as-well
368
                if path.endswith('.py') and m(path + 'c', conditions):
369
                    Log.Info('(%s) %s: "%s"', name, action, path + 'c')
370
371
372
class ForceLegacy(Migration):
373
    """Migrates the 'force_legacy' option to the 'activity_mode' option."""
374
375
    def run(self):
376
        self.upgrade()
377
378
    def upgrade(self):
379
        if not os.path.exists(self.preferences_path):
380
            Log.Error('Unable to find preferences file at "%s", unable to run migration', self.preferences_path)
381
            return
382
383
        preferences = self.get_preferences()
384
385
        # Read 'force_legacy' option from raw preferences
386
        force_legacy = preferences.get('force_legacy')
387
388
        if force_legacy is None:
389
            return
390
391
        force_legacy = force_legacy.lower() == "true"
392
393
        if not force_legacy:
394
            return
395
396
        # Read 'activity_mode' option from raw preferences
397
        activity_mode = preferences.get('activity_mode')
398
399
        # Activity mode has already been set, not changing it
400
        if activity_mode is not None:
401
            return
402
403
        self.set_preferences({
404
            'activity_mode': '1'
405
        })
406
407
408
class SelectiveSync(Migration):
409
    """Migrates the syncing task bool options to selective synchronize/push/pull enums"""
410
411
    option_keys = [
412
        'sync_watched',
413
        'sync_ratings',
414
        'sync_collection'
415
    ]
416
417
    value_map = {
418
        'false': '0',
419
        'true': '1',
420
    }
421
422
    def run(self):
423
        self.upgrade()
424
425
    def upgrade(self):
426
        preferences = self.get_preferences()
427
428
        # Filter to only relative preferences
429
        preferences = dict([
430
            (key, value)
431
            for key, value in preferences.items()
432
            if key in self.option_keys
433
        ])
434
435
        changes = {}
436
437
        for key, value in preferences.items():
438
            if value not in self.value_map:
439
                continue
440
441
            changes[key] = self.value_map[value]
442
443
        if not changes:
444
            return
445
446
        Log.Debug('Updating preferences with changes: %s', changes)
447
        self.set_preferences(changes)
448
449
450
FSMigrator.register(Clean)
451
FSMigrator.register(ForceLegacy)
452
FSMigrator.register(SelectiveSync)
453