engforge 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. engforge/__init__.py +26 -0
  2. engforge/_testing_components.py +864 -0
  3. engforge/analysis.py +257 -0
  4. engforge/attr_dynamics.py +222 -0
  5. engforge/attr_plotting.py +811 -0
  6. engforge/attr_signals.py +116 -0
  7. engforge/attr_slots.py +211 -0
  8. engforge/attr_solver.py +443 -0
  9. engforge/attributes.py +326 -0
  10. engforge/common.py +76 -0
  11. engforge/component_collections.py +276 -0
  12. engforge/components.py +55 -0
  13. engforge/configuration.py +673 -0
  14. engforge/dataframe.py +185 -0
  15. engforge/datastores/__init__.py +116 -0
  16. engforge/datastores/data.py +519 -0
  17. engforge/datastores/gdocs.py +2650 -0
  18. engforge/datastores/reporting.py +935 -0
  19. engforge/datastores/secrets.py +549 -0
  20. engforge/dynamics.py +662 -0
  21. engforge/eng/__init__.py +0 -0
  22. engforge/eng/costs.py +1459 -0
  23. engforge/eng/fluid_material.py +279 -0
  24. engforge/eng/geometry.py +1060 -0
  25. engforge/eng/pipes.py +1211 -0
  26. engforge/eng/prediction.py +366 -0
  27. engforge/eng/solid_materials.py +445 -0
  28. engforge/eng/structure.py +2228 -0
  29. engforge/eng/structure_beams.py +821 -0
  30. engforge/eng/thermodynamics.py +271 -0
  31. engforge/engforge_attributes.py +380 -0
  32. engforge/env_var.py +214 -0
  33. engforge/locations.py +18 -0
  34. engforge/logging.py +290 -0
  35. engforge/patterns.py +449 -0
  36. engforge/problem_context.py +2251 -0
  37. engforge/properties.py +501 -0
  38. engforge/reporting.py +595 -0
  39. engforge/solveable.py +964 -0
  40. engforge/solver.py +316 -0
  41. engforge/solver_utils.py +726 -0
  42. engforge/system.py +143 -0
  43. engforge/system_reference.py +299 -0
  44. engforge/tabulation.py +288 -0
  45. engforge/test/__init__.py +2 -0
  46. engforge/test/_pre_test_structures.py +221 -0
  47. engforge/test/report_testing.py +158 -0
  48. engforge/test/test_airfilter.py +91 -0
  49. engforge/test/test_analysis.py +3 -0
  50. engforge/test/test_comp_iter.py +196 -0
  51. engforge/test/test_composition.py +139 -0
  52. engforge/test/test_costs.py +338 -0
  53. engforge/test/test_dynamics.py +109 -0
  54. engforge/test/test_dynamics_spaces.py +39 -0
  55. engforge/test/test_four_bar.py +97 -0
  56. engforge/test/test_modules.py +72 -0
  57. engforge/test/test_performance.py +93 -0
  58. engforge/test/test_pipes.py +49 -0
  59. engforge/test/test_problem.py +411 -0
  60. engforge/test/test_problem_deepscoping.py +68 -0
  61. engforge/test/test_slider_crank.py +51 -0
  62. engforge/test/test_solver.py +343 -0
  63. engforge/test/test_tabulation.py +233 -0
  64. engforge/typing.py +92 -0
  65. engforge-0.3.0.dist-info/METADATA +392 -0
  66. engforge-0.3.0.dist-info/RECORD +69 -0
  67. engforge-0.3.0.dist-info/WHEEL +5 -0
  68. engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
  69. engforge-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2650 @@
1
+ import os, sys
2
+
3
+ from engforge.common import *
4
+ import engforge
5
+
6
+
7
+ from engforge.logging import LoggingMixin, logging
8
+ from engforge.locations import client_path
9
+
10
+ from concurrent.futures import ThreadPoolExecutor
11
+ import threading
12
+
13
+ from contextlib import contextmanager
14
+ import pygsheets
15
+ import pydrive2
16
+ import traceback
17
+ import pathlib
18
+ import googleapiclient
19
+ import functools
20
+ from pydrive2.auth import GoogleAuth, ServiceAccountCredentials
21
+ from pydrive2.drive import GoogleDrive
22
+ import time
23
+ import json
24
+ import attr
25
+ import datetime
26
+ import random
27
+ from pathlib import Path
28
+ import networkx as nx
29
+ from networkx_query import search_nodes, search_edges
30
+ import pickle
31
+ from expiringdict import (
32
+ ExpiringDict,
33
+ ) # To aid dynamic programming, store network calls they are expensive
34
+
35
+ import concurrent
36
+ from ray.runtime_context import get_runtime_context
37
+
38
+ log = logging.getLogger("engforge-gdocs")
39
+
40
+ # TODO: GoogleAuth.DEFAULT_SETTINGS["client_config_file"] = google_api_token()
41
+
42
+ # Filed will be None until assigned an folder id
43
+ STANDARD_FOLDERS = {
44
+ "ClientFolders": None,
45
+ "InternalClientDocuments": None,
46
+ "engforge": None,
47
+ "Research": None,
48
+ }
49
+
50
+ # TODO: make env_vars:
51
+ # 1. CLIENT_GDRIVE_PATH
52
+
53
+ # We'll hook this up with functionality at the module level
54
+ global CLIENT_G_DRIVE, CLIENT_GDRIVE_SYNC, CLIENT_GMAIL, CLIENT_NAME
55
+
56
+
57
+ class OtterDriveException(Exception):
58
+ pass
59
+
60
+
61
+ class CredsNotFound(OtterDriveException):
62
+ pass
63
+
64
+
65
+ class SharedDriveNotFound(OtterDriveException):
66
+ pass
67
+
68
+
69
+ class SourceFolderNotFound(OtterDriveException):
70
+ pass
71
+
72
+
73
+ class SyncPathNotFound(OtterDriveException):
74
+ pass
75
+
76
+
77
+ # Node Classes
78
+ class FileNode(LoggingMixin):
79
+ """A wrapper for fileMeta objects to work in a graph and store conveinence functions"""
80
+
81
+ _drive = None
82
+ _item = None
83
+
84
+ _absolute_path = None
85
+
86
+ def __init__(self, drive, item_meta):
87
+ self._item = item_meta
88
+ self._drive = drive
89
+
90
+ self.debug(f"Created Node {self.title}")
91
+
92
+ self.close_http()
93
+
94
+ # if not threading.main_thread(): #save some time yo!
95
+ # try:
96
+ # if self in self.drive.item_cache:
97
+ # self.absolute_path
98
+
99
+ # except Exception as e:
100
+ # self.error(e,f'Issue caching path for {self.identity}')
101
+
102
+ @property
103
+ def id(self):
104
+ return self.item["id"]
105
+
106
+ @property
107
+ def title(self):
108
+ return self.item["title"]
109
+
110
+ @property
111
+ def labels(self):
112
+ if "labels" in self.item:
113
+ return self.item["labels"]
114
+
115
+ @property
116
+ def createdDate(self):
117
+ if "createdDate" in self.item:
118
+ return self.get_datetime(self.item["createdDate"])
119
+
120
+ @property
121
+ def modifiedDate(self):
122
+ if "modifiedDate" in self.item:
123
+ return self.get_datetime(self.item["modifiedDate"])
124
+
125
+ @property
126
+ def listed_shared_drive_id(self):
127
+ if "teamDriveId" in self.item:
128
+ return self.item["teamDriveId"]
129
+
130
+ @property
131
+ def parents(self):
132
+ # with self.filesystem as fs:
133
+ nodes = self.drive.item_nodes
134
+ return [
135
+ nodes[key]
136
+ for key in list(self._drive._filesystem.predecessors(self))
137
+ if key in nodes
138
+ ]
139
+
140
+ @property
141
+ def contents(self):
142
+ # with self.filesystem as fs:
143
+ nodes = self.drive.item_nodes
144
+ return [
145
+ nodes[key]
146
+ for key in list(self._drive._filesystem.neighbors(self))
147
+ if key in nodes
148
+ ]
149
+
150
+ @property
151
+ def listed_parents(self):
152
+ if "parents" in self.item:
153
+ return self.item["parents"]
154
+ return []
155
+
156
+ @property
157
+ def item_is_root(self):
158
+ return False
159
+
160
+ @property
161
+ def item_in_root(self):
162
+ return any([par["isRoot"] for par in self.listed_parents])
163
+
164
+ @property
165
+ def is_drivefile(self):
166
+ if self.item is None:
167
+ return False
168
+ if "id" not in self.item:
169
+ return False
170
+ if "kind" in self.item:
171
+ if self.item["kind"] == "drive#file":
172
+ return True
173
+ return False
174
+
175
+ @property
176
+ def is_folder(self):
177
+ if self.is_drivefile:
178
+ if any(
179
+ [
180
+ self.item["mimeType"] == "application/vnd.google-apps.folder",
181
+ self.item["mimeType"] == "application/vnd.google-apps.shortcut",
182
+ ]
183
+ ):
184
+ return True
185
+ return False
186
+
187
+ @property
188
+ def is_file(self):
189
+ if self.is_drivefile:
190
+ if not any(
191
+ [
192
+ self.item["mimeType"] == "application/vnd.google-apps.folder",
193
+ self.item["mimeType"] == "application/vnd.google-apps.shortcut",
194
+ ]
195
+ ):
196
+ return True
197
+ return False
198
+
199
+ @property
200
+ def is_protected(self):
201
+ protected_keys = ["starred", "trashed", "hidden"]
202
+
203
+ if self.is_drivefile:
204
+ labels = self.labels
205
+
206
+ if labels is None:
207
+ return True # no metadata?
208
+
209
+ # Is Starred, Trashed, Restricted or Hidden
210
+ if any([labels[pk] for pk in protected_keys]):
211
+ self.debug(f"file has protected keys {labels}")
212
+ return True
213
+ # Protected Folder Name
214
+ if self.title in self.drive.protected_filenames and self.is_folder:
215
+ self.debug(f'folder has a protected filename {self.item["title"]}')
216
+ return True
217
+ # File Id is protected
218
+ elif self.id in self.drive.protected_ids:
219
+ self.debug(f'fil has a protected id {self.item["id"]}')
220
+ return True
221
+ # Its a share drive, not possible
222
+ elif self.item_is_root:
223
+ return True
224
+ # Parent Is Root
225
+ elif self.item_in_root:
226
+ self.debug(f"fil is in root")
227
+ return True
228
+
229
+ # If these dont match its fair game!
230
+ return False
231
+
232
+ @property
233
+ def is_valid(self):
234
+ if self.title.startswith("."):
235
+ return False
236
+
237
+ if self.is_folder:
238
+ if self.title.startswith("__") or self.title.endswith("__"):
239
+ return False
240
+ return True
241
+
242
+ def remove_contents_with_title(self, title):
243
+ for item in self.contents:
244
+ if item.title == title:
245
+ self.info(f"removing item {item.id} with title {title}")
246
+ item.delete()
247
+
248
+ # Private wrappers
249
+ @property
250
+ def filesystem(self):
251
+ return self._drive.filesystem
252
+
253
+ @property
254
+ def drive(self):
255
+ return self._drive
256
+
257
+ @property
258
+ def item(self):
259
+ return self._item
260
+
261
+ @property
262
+ def attributes(self):
263
+ return {
264
+ "title": self.title,
265
+ "created": self.createdDate,
266
+ "folder": self.is_folder,
267
+ "protected": self.is_protected,
268
+ }
269
+
270
+ @property
271
+ def best_nodeid_paths(self):
272
+ try:
273
+ with self.filesystem as fs:
274
+ return list(
275
+ nx.all_shortest_paths(
276
+ fs, source=self.drive.sync_root_id, target=self.id
277
+ )
278
+ )
279
+ except Exception as e:
280
+ self.error(e)
281
+ return None
282
+
283
+ @property
284
+ def absolute_paths(self):
285
+ npaths = self.best_nodeid_paths
286
+ if npaths:
287
+ return [
288
+ os.path.join(*[self.drive.item_nodes[itdd].title for itdd in path_list])
289
+ for path_list in npaths
290
+ ]
291
+
292
+ @property
293
+ def best_nodeid_path(self):
294
+ try:
295
+ with self.filesystem as fs:
296
+ return list(
297
+ nx.shortest_path(fs, source=self.drive.sync_root_id, target=self.id)
298
+ )
299
+ except Exception as e:
300
+ self.error(e)
301
+ return None
302
+
303
+ @property
304
+ def parents_path(self):
305
+ # WIP: this should be faster
306
+ for parent in self.parents:
307
+ return parent.parents_path + [self.id]
308
+ else:
309
+ return [self.id]
310
+
311
+ @property
312
+ def absolute_path(self):
313
+ if self._absolute_path is None:
314
+ parents = self.parents
315
+
316
+ if parents:
317
+ self._absolute_path = os.path.join(parents[0].absolute_path, self.title)
318
+ # self.debug(f'caching file abspath for node {self._absolute_path}')
319
+ else:
320
+ self._absolute_path = self.title
321
+ # self.info(f'caching file abspath for root node {self._absolute_path}')
322
+
323
+ return self._absolute_path
324
+
325
+ @property
326
+ def identity(self):
327
+ return f"FN:{self.id}"[:24]
328
+
329
+ def get_datetime(self, dts: str):
330
+ return datetime.datetime.strptime(dts, "%Y-%m-%dT%H:%M:%S.%fZ")
331
+
332
+ def close_http(self):
333
+ """to use multi-threads we must close connections"""
334
+ if self._item.http is not None:
335
+ for c in self._item.http.connections.values():
336
+ c.close()
337
+ del self._item.http
338
+ self._item.http = None
339
+
340
+ def ensure_http(self):
341
+ """close and create new connection to ensure that its in the right context in thread"""
342
+ if self._item.http is not None:
343
+ self.close_http()
344
+ self._item.http = self.drive.gauth.Get_Http_Object()
345
+
346
+ def delete(self):
347
+ self.ensure_http()
348
+ if not self.is_protected and not self.drive.dry_run:
349
+ with self.drive.rate_limit_manager(self.delete, 2, gfileMeta=self.item):
350
+ self.info(f"deleting: {self.title}")
351
+ self.item.Trash()
352
+ self.drive.sleep()
353
+ self.drive.removeNode(self)
354
+ else:
355
+ if self.is_protected:
356
+ self.debug(f"could not delete protected {self.title} ")
357
+ elif self.drive.dry_run:
358
+ self.debug(f"deleting: {self.title}")
359
+ self.drive.removeNode(self)
360
+
361
+ # Def network x integration (hash)
362
+ def __hash__(self):
363
+ return hash(self.id)
364
+
365
+ def __eq__(self, other):
366
+ if isinstance(other, FileNode):
367
+ return self.id == other.id
368
+ elif isinstance(other, str):
369
+ return self.id == other
370
+ self.warning(f"clould not compare {other} to {self}")
371
+ return False
372
+
373
+ def __str__(self):
374
+ return f"{self.id}|{self.title}"
375
+
376
+ def __repr__(self):
377
+ return f"FileNode({self.id}|{self.title})"
378
+
379
+ # Magic Methods (Pickling here)
380
+ def __getstate__(self):
381
+ """Remove active connection objects, they are not picklable"""
382
+ # self.debug('removing unpicklable info')
383
+ d = self.__dict__.copy()
384
+ # d['_drive'] = None
385
+ d["_log"] = None
386
+ d["drive_args"] = self._drive.initial_args
387
+
388
+ if isinstance(
389
+ d["_item"], pydrive2.files.GoogleDriveFile
390
+ ): # catch shared drives and skip
391
+ d["_item"].__dict__["attr"]["auth"] = None
392
+
393
+ return d
394
+
395
+ def __setstate__(self, d):
396
+ """We reconfigure on opening a pickle"""
397
+ self.__dict__ = d
398
+ self.debug("seralizing")
399
+
400
+
401
+ class RootNode(FileNode):
402
+ """A wrapper for shared drive objects to work in a graph and store conveinence functions"""
403
+
404
+ _drive = None
405
+ _title = None
406
+ _id = None
407
+ _item = None
408
+
409
+ def __init__(self, drive, title, id):
410
+ self._id = id
411
+ self._drive = drive
412
+ self._title = title
413
+ self._item = {}
414
+
415
+ self.debug(f"Mapped Share Drive {self.title}:{self.identity}")
416
+
417
+ @property
418
+ def id(self):
419
+ return self._id
420
+
421
+ @property
422
+ def title(self):
423
+ return f"shared:{self._title}"
424
+
425
+ @property
426
+ def is_drivefile(self):
427
+ return True
428
+
429
+ @property
430
+ def is_folder(self):
431
+ return True
432
+
433
+ @property
434
+ def is_file(self):
435
+ return True
436
+
437
+ @property
438
+ def item_is_root(self):
439
+ return True
440
+
441
+ @property
442
+ def is_protected(self):
443
+ return True
444
+
445
+ @property
446
+ def is_valid(self):
447
+ return True
448
+
449
+ def __repr__(self):
450
+ return f"SharedDrive({self.id}|{self.title})"
451
+
452
+ @property
453
+ def best_nodeid_paths(self):
454
+ return [[]]
455
+
456
+ @property
457
+ def absolute_paths(self):
458
+ return [[self.title]]
459
+
460
+ @property
461
+ def best_nodeid_path(self):
462
+ return []
463
+
464
+ @property
465
+ def absolute_path(self):
466
+ return self.title
467
+
468
+
469
+ default_timestamp = 1577858461 # jan 1 2020
470
+ timestamp_divider = 24 * 3600 # a day in seconds
471
+
472
+ days_since_2020 = (
473
+ lambda item: (item.createdDate.timestamp() - default_timestamp) / timestamp_divider
474
+ )
475
+ content_size = lambda item: len(item.contents)
476
+
477
+
478
+ # FIXME: Implement Thread Saftey To Prevent Rare Segmentation Faults
479
+
480
+
481
+ # @Singleton
482
+ class OtterDrive(LoggingMixin, metaclass=InputSingletonMeta):
483
+ """Authenticates To Your Google Drive To Edit Sheets and Your Drive Files
484
+
485
+ OtterDrive is a input based singleton so insantiated: OtterDrive(**args1) and you can do this
486
+ anywhere in the same code and preserve cached information for other access to OtterDirve(**args1), however its not threadsafe (yet)
487
+
488
+ OtterDrive is tested to work with shared drives and service accounts, portions of the code may work with user drives. These drives start with shared:DRIVENAME, and the default is shared:OTTERBOX.
489
+ """
490
+
491
+ gauth = None
492
+ gsheets = None
493
+ gdrive = None
494
+
495
+ dry_run = False
496
+ initalized = False
497
+
498
+ # Graph Of Filenetwork
499
+ _filesystem = None
500
+
501
+ # Storage Items
502
+ protected_ids = None
503
+ protected_filenames = None
504
+
505
+ # Determined Items
506
+ _creds_file = None
507
+ _shared_drives = None
508
+
509
+ # Input items
510
+ _sync_root = None
511
+ _filepath_root = None
512
+ _shared_drive = None
513
+
514
+ # Derived From Input
515
+ _sync_root_id = None # For Shared Drive Root
516
+ _target_folder_id = None # For Sync Path
517
+
518
+ call_count = 0 # updated every sleep()
519
+ max_sleep_time = 5.0
520
+ _sleep_time = 0.25
521
+ time_fuzz = 1.0 # base * ( 1+ rand(0,time_fuzz))
522
+ _absolute_min_sleep = 0.1
523
+ thread_time_multiplier = 1.0
524
+
525
+ # Default is most permissive
526
+ explict_input_only = False
527
+ guess_sync_path = True
528
+
529
+ # Thread Pool
530
+ _use_threadpool = True
531
+ _max_num_threads = 10
532
+
533
+ # defaults
534
+ default_shared_drive = "shared:OTTERBOX"
535
+ default_sync_path = "ClientFolders"
536
+ default_service_id = "{engforge}@{role}.iam.gserviceaccount.com"
537
+ creds_file_name = "engforge_gdocs_serviceid.json"
538
+
539
+ filepath_inferred = False # sets on filepath_root.setter
540
+
541
+ net_lock = None
542
+
543
+ log_fmt = "[%(name)-12s %(threadName)s]%(message)s"
544
+
545
+ fast_cache = None # To handle expensive graph calls
546
+
547
+ initial_args = None
548
+ init_after_read = False
549
+
550
+ # attributes to keep after a pickle read
551
+ keep_keys = [
552
+ "_filesystem",
553
+ "protected_ids",
554
+ "protected_filenames",
555
+ "_creds_file",
556
+ "_shared_drives",
557
+ "_shared_drive",
558
+ "_sync_root_id",
559
+ ]
560
+
561
+ def __init__(
562
+ self,
563
+ shared_drive=None,
564
+ sync_root=None,
565
+ filepath_root=None,
566
+ creds_path=None,
567
+ dry_run=False,
568
+ num_workers=10,
569
+ use_threadpool=True,
570
+ explict_input_only=False,
571
+ ):
572
+ """
573
+ :param shared_drive: share drive to use as a root directory
574
+ :param sync_root: the relative directory from shared_drive root to the place where sync occurs
575
+ :param filepath_root: the local machine file path to sync to google drive
576
+ :param creds_path: can be none if filesys if configured, otherwise you can input a path to a json, or a folder to look for `engforgegdocs_serviceid.json`
577
+ """
578
+
579
+ self.initial_args = dict(
580
+ shared_drive=shared_drive,
581
+ sync_root=sync_root,
582
+ filepath_root=filepath_root,
583
+ creds_path=creds_path,
584
+ dry_run=dry_run,
585
+ num_workers=num_workers,
586
+ use_threadpool=use_threadpool,
587
+ explict_input_only=explict_input_only,
588
+ )
589
+
590
+ self.explict_input_only = explict_input_only
591
+
592
+ self.info(
593
+ f"starting with input share={shared_drive}, filepath={filepath_root}, sync_root={sync_root}"
594
+ )
595
+
596
+ self._use_threadpool = use_threadpool
597
+ self._max_num_threads = num_workers
598
+
599
+ self._hot_directories = ExpiringDict(max_len=100, max_age_seconds=1)
600
+
601
+ # This is to guard access across threads to the filesystem networkx, which is not threadsafe
602
+ # TODO: load graph from disk if exists
603
+ self.net_lock = threading.RLock()
604
+ self._filesystem = nx.DiGraph()
605
+
606
+ self.protected_ids = set()
607
+ self.protected_filenames = list(STANDARD_FOLDERS.keys())
608
+
609
+ self.dry_run = dry_run
610
+
611
+ # Handle Different Creds Input
612
+ if creds_path:
613
+ if creds_path.endswith(".json"):
614
+ self.creds_file = creds_path
615
+ else:
616
+ self.creds_file = os.path.join(creds_path, self.creds_file_name)
617
+ else:
618
+ self.creds_file = os.path.join(creds_folder(), self.creds_file_name)
619
+
620
+ # Authoirize yoself foo
621
+ self.authoirze_google_integrations()
622
+
623
+ self.info("Setting Up Otterdrive...")
624
+ # These use setters via properties so order is important
625
+ # 0) Get Share Drive Info
626
+ self.update_shared_drives()
627
+ # 1) set and check share drive and check other souces for input
628
+ self.shared_drive = shared_drive
629
+ # 2) set and check filepath infer from current directory if not input
630
+ self.filepath_root = filepath_root
631
+ # 3) set and check sync path and infer from #1 / 2 if not input
632
+ self.sync_root = sync_root
633
+
634
+ self.initalize()
635
+
636
+ @property
637
+ def use_threadpool(self):
638
+ if self.is_ray_context:
639
+ return False
640
+ return self._use_threadpool
641
+
642
+ @property
643
+ @contextmanager
644
+ def filesystem(self):
645
+ if self.use_threadpool:
646
+ self.net_lock.acquire()
647
+ try:
648
+ # TODO: add a saving mechanism on disk overtime
649
+ yield self._filesystem
650
+ except Exception as e:
651
+ self.error(e, "Error Accessing Filesystem")
652
+ finally:
653
+ if self.use_threadpool:
654
+ self.net_lock.release()
655
+
656
+ @property
657
+ def initalized_values(self):
658
+ return [
659
+ self._sync_root_id,
660
+ self._target_folder_id,
661
+ self.sync_root,
662
+ self.gauth,
663
+ self.gsheets,
664
+ self.gdrive,
665
+ ]
666
+
667
+ @property
668
+ def is_initalized(self):
669
+ tests = list([val is not None for val in self.initalized_values])
670
+ return all(tests)
671
+
672
+ # Initalization Methods
673
+ def initalize(self, ttl=1, reinit=False):
674
+ """Initalize maps the google root and shared folders, adds protections, and find the sync target"""
675
+
676
+ self.info(f"Initalize({ttl})")
677
+ # did_read = self.read() #READ FS HERE
678
+ # if not did_read:
679
+
680
+ self.thread_time_multiplier = 1.0
681
+ gpath = self.sync_path(self.filepath_root)
682
+
683
+ node_ids = set(self.item_nodes.keys())
684
+
685
+ # First level is protected!
686
+ if not self.initalized or reinit:
687
+ self.info(f"Initalizing from {self.filepath_root} -> {gpath}")
688
+ self.sync_folder_contents_locally(self.sync_root_id, protect=True)
689
+ target = self.sync_folder_contents_locally(
690
+ self.sync_root_id,
691
+ stop_when_found=gpath,
692
+ recursive=True,
693
+ ttl=int(5),
694
+ )
695
+
696
+ self.gsheets.drive.enable_team_drive(self.sync_root_id)
697
+
698
+ if target is not None and target.id in self.item_cache:
699
+ self._target_folder_id = target.id
700
+ elif gpath:
701
+ self._target_folder_id = self.ensure_g_path_get_id(gpath)
702
+ elif gpath == "":
703
+ self._target_folder_id = self.sync_root_id
704
+
705
+ self.initalized = True
706
+
707
+ else:
708
+ if gpath:
709
+ self._target_folder_id = self.ensure_g_path_get_id(gpath)
710
+ elif gpath == "":
711
+ self._target_folder_id = self.sync_root_id
712
+
713
+ node_ids = set(self.item_nodes.keys())
714
+
715
+ # cache when we dont have that info
716
+ # if self.target_folder_id not in node_ids:
717
+ # self.sync_folder_contents_locally(self.target_folder_id, recursive=True, ttl=int(1E6))
718
+ # else:
719
+ # self.sync_folder_contents_locally(self.target_folder_id, recursive=True, ttl=ttl) #fully refresh the top levels
720
+
721
+ self.status_message("Otterdrive Ready!")
722
+ self.thread_time_multiplier = 1.0
723
+
724
+ # self.save()
725
+
726
+ def reset_target_id(self):
727
+ self._target_folder_id = self.ensure_g_path_get_id(
728
+ self.sync_path(self.filepath_root)
729
+ )
730
+ self.sync_folder_contents_locally(self.target_folder_id)
731
+
732
+ def status_message(self, header=""):
733
+ self.debug(
734
+ f"{header}:\nSharedDrive: {self.shared_drive}:{self.sync_root_id}\nTarget Folder: {self.sync_root}:{self.target_folder_id}\nFilePath Conversion: {self.filepath_root}->{self.full_sync_root}"
735
+ )
736
+
737
+ def authoirze_google_integrations(self, retry=True, ttl=3):
738
+ try:
739
+ self.sleep(self._sleep_time + 3 * random.random())
740
+ self.debug("Authorizing...")
741
+ # Drive Authentication Using Service Account
742
+ scope = ["https://www.googleapis.com/auth/drive"]
743
+
744
+ self.gauth = GoogleAuth()
745
+ self.sleep()
746
+
747
+ self.gauth.auth_method = "service"
748
+ self.gauth.credentials = ServiceAccountCredentials.from_json_keyfile_name(
749
+ self.creds_file, scope
750
+ )
751
+
752
+ self.gauth.Authorize()
753
+ self.sleep()
754
+
755
+ # Do Sheets Authentication
756
+ self.gsheets = pygsheets.authorize(service_account_file=self.creds_file)
757
+ self.sleep()
758
+
759
+ self.gdrive = GoogleDrive(self.gauth)
760
+ self.sleep()
761
+
762
+ self.gdrive.http = (
763
+ self.gauth.Get_Http_Object()
764
+ ) # Do in advance for share drives
765
+ self.sleep()
766
+
767
+ self.info("Authorized!")
768
+
769
+ except Exception as e:
770
+ ttl -= 1
771
+ if retry and ttl > 0:
772
+ self.hit_rate_limit()
773
+ self.warning(f"authorize failed {str(e)}")
774
+ self.authoirze_google_integrations(retry=True, ttl=ttl)
775
+ elif retry and ttl == 0:
776
+ self.hit_rate_limit()
777
+ self.warning(f"authorize failed last try {str(e)}")
778
+ self.authoirze_google_integrations(retry=False, ttl=ttl - 1)
779
+ else:
780
+ self.error(e, "authorize failed!!!")
781
+
782
+ def reset(self):
783
+ # Never replace these values if already configured (that would be redoing work)
784
+ self._filesystem = nx.DiGraph()
785
+
786
+ self.protected_ids = set()
787
+ self.protected_filenames = list(STANDARD_FOLDERS.keys())
788
+
789
+ self.authoirze_google_integrations()
790
+ self.update_shared_drives()
791
+ self.initalize()
792
+
793
+ def sync_path(self, path):
794
+ """Sync path likes absolute paths to google drive relative"""
795
+ assert os.path.commonpath([self.filepath_root, path]) == os.path.commonpath(
796
+ [self.filepath_root]
797
+ )
798
+
799
+ self.debug(f"finding relative path from {path} and {self.filepath_root}")
800
+ rel_root = os.path.relpath(path, self.filepath_root)
801
+
802
+ # self.debug(f'getting gdrive path {self.full_sync_root} and {rel_root}')
803
+ gdrive_root = os.path.join(self.full_sync_root, rel_root)
804
+
805
+ # remove current directory /.
806
+ if gdrive_root.endswith("{}.".format(os.sep)):
807
+ gdrive_root = os.path.split(gdrive_root)[0]
808
+
809
+ if gdrive_root.startswith(os.sep):
810
+ gdrive_root = gdrive_root.replace(os.sep, "", 1)
811
+
812
+ return gdrive_root
813
+
814
+ @property
815
+ def shared_drives(self):
816
+ if self._shared_drives is None:
817
+ self.update_shared_drives()
818
+ return self._shared_drives
819
+
820
+ def update_shared_drives(self):
821
+ """returns the shared drives associated with the service account"""
822
+ meta, content = self.gdrive.http.request(
823
+ "https://www.googleapis.com/drive/v3/drives"
824
+ )
825
+ drives = json.loads(content)["drives"]
826
+ output = {}
827
+ for drive in drives:
828
+ rnode = RootNode(self, drive["name"], drive["id"])
829
+ self.addFileNode(rnode)
830
+
831
+ output[drive["name"]] = drive["id"]
832
+
833
+ # self._folder_ids[f"shared:{drive['name']}"] = drive['id']
834
+
835
+ # self.sync_folder_contents_locally(drive['id'])
836
+ self._shared_drives = output
837
+ return output
838
+
839
+ # FIXME: This doesn't work, we don't stop anywhere with stop_when_found
840
+ def sync_folder_contents_locally(
841
+ self,
842
+ parent_id,
843
+ stop_when_found=None,
844
+ recursive=False,
845
+ ttl=1,
846
+ protect=False,
847
+ pool=None,
848
+ already_cached=None,
849
+ top=True,
850
+ ):
851
+ """This function takes a parent id for a folder then caches everything to folder / file caches
852
+ Recrusvie functionality with recursive=True and ttl > 0"""
853
+ # Update Subdirectories - more efficient all at once
854
+
855
+ self.debug(f"updating subdirecories of {parent_id}")
856
+ stop = False
857
+ result = None
858
+
859
+ # create uniform recursive object
860
+ if top:
861
+ self.info(
862
+ f"sync local with args pid={parent_id}, stop={stop_when_found}, rec={recursive},ttl={ttl},protect={protect}"
863
+ )
864
+ init_val = bool(recursive)
865
+
866
+ class token:
867
+ recursive = init_val
868
+
869
+ obj = token()
870
+ obj.recursive = recursive
871
+ else:
872
+ obj = recursive
873
+
874
+ if stop_when_found is not None:
875
+ self.info(f"Stopping When Found: returning id of {stop_when_found}")
876
+
877
+ if protect:
878
+ self.protected_ids.add(parent_id)
879
+
880
+ if pool is None: # and self.use_threadpool #Always use threads for this!
881
+ if top:
882
+ self.debug("GET DRIVE INFO USING THREADS!!!")
883
+ pool = ThreadPoolExecutor(max_workers=self.num_threads)
884
+
885
+ else:
886
+ if top:
887
+ self.debug("GET DRIVE INFO SINGLE THREAD")
888
+ pool = None # the second time
889
+
890
+ if already_cached is None: # and self.use_threadpool :
891
+ already_cached = set()
892
+
893
+ try:
894
+ # This caches it for us through seach_item(), and hopefully removes duplicates
895
+ items = list(self.all_in_folder(parent_id))
896
+
897
+ if parent_id in self.item_nodes:
898
+ par = self.item_nodes[parent_id]
899
+ par_path = par.absolute_path
900
+ if par_path == stop_when_found:
901
+ self.info(f"Found {stop_when_found}")
902
+ stop = True
903
+ result = self.item_nodes[parent_id]
904
+ obj.recursive = False
905
+ if pool is not None:
906
+ pool.shutdown(wait=False) # force shutdown
907
+ pool = None
908
+ else:
909
+ self.info(f"searching sync {par_path}...")
910
+
911
+ if obj.recursive and ttl > 0:
912
+ ttl -= 1
913
+ for item in items:
914
+ self.sleep()
915
+
916
+ if (
917
+ stop_when_found is not None
918
+ and item.absolute_path == stop_when_found
919
+ ):
920
+ if not stop:
921
+ self.info(f"Found {stop_when_found}")
922
+ stop = True
923
+ result = item
924
+ obj.recursive = False
925
+ if pool is not None:
926
+ pool.shutdown(wait=False) # force shutdown
927
+ pool = None
928
+ else: # this is a duplicate!
929
+ self.warning(
930
+ f"duplicate found {result.identity} vs {item.identity}"
931
+ )
932
+ # TODO: handle duplicates
933
+
934
+ if item.is_folder: # if it had contents it was already cached!!
935
+ # Either do not stop or ensure that the item path is in the target
936
+ if (
937
+ stop_when_found is None
938
+ or item.absolute_path in stop_when_found
939
+ ):
940
+ if obj.recursive and not stop:
941
+ if item.id not in already_cached:
942
+ already_cached.add(item.id)
943
+
944
+ # recursive covers next, but will sync the directory contents, keeping the sync level global
945
+ if ttl <= 0:
946
+ obj.recursive = False
947
+
948
+ if pool is not None:
949
+ self.sleep()
950
+ if ttl >= 0:
951
+ pool.submit(
952
+ self.sync_folder_contents_locally,
953
+ item.id,
954
+ recursive=obj,
955
+ ttl=ttl,
956
+ protect=protect,
957
+ pool=pool,
958
+ already_cached=already_cached,
959
+ top=False,
960
+ stop_when_found=stop_when_found,
961
+ )
962
+ else:
963
+ if ttl >= 0:
964
+ self.sync_folder_contents_locally(
965
+ item.id,
966
+ recursive=obj,
967
+ ttl=ttl,
968
+ protect=protect,
969
+ already_cached=already_cached,
970
+ top=False,
971
+ stop_when_found=stop_when_found,
972
+ )
973
+
974
+ except Exception as e:
975
+ self.error(e, "Issue Syncing Locally")
976
+
977
+ finally:
978
+ if pool is not None:
979
+ pool.shutdown()
980
+
981
+ return result
982
+
983
+ @contextmanager
984
+ def rate_limit_manager(self, retry_function, multiplier, *args, **kwargs):
985
+ """A context manager that handles authentication and rate limit errors"""
986
+
987
+ if "gfileMeta" in kwargs:
988
+ file = kwargs.pop("gfileMeta")
989
+ else:
990
+ file = None
991
+
992
+ # def retry(function,*args,**kwargs):
993
+ # with self.rate_limit_manger(retry_function,multiplier,*args,**kwargs):
994
+ # return function(*args,**kwargs)
995
+
996
+ try:
997
+ yield self # We just want error handiling, context should be useful
998
+
999
+ # except OSError as e:
1000
+ # if e.errno == 101:
1001
+ # self.hit_rate_limit(60.0 + 30.0*random.random())
1002
+ # return retry_function(*args,**kwargs)
1003
+ # else:
1004
+ # self.error(e,'Unexpected OSError:')
1005
+
1006
+ except ConnectionError as err:
1007
+ self.info("connection reset, retrying auth")
1008
+ self.hit_rate_limit(30.0 + 30.0 * random.random(), multiplier=multiplier)
1009
+ self.authoirze_google_integrations()
1010
+ self.sleep(10.0 + 10.0 * random.random())
1011
+ return retry_function(*args, **kwargs)
1012
+
1013
+ except googleapiclient.errors.HttpError as err:
1014
+ # If the error is a rate limit or connection error,
1015
+ # wait and try again.
1016
+
1017
+ # Close connection to existing file
1018
+ if file is not None and file.http is not None:
1019
+ for c in file.http.connections.values():
1020
+ c.close()
1021
+ if err.resp.status in [429]:
1022
+ self.hit_rate_limit(
1023
+ 30.0 + 10.0 * random.random(), multiplier=multiplier
1024
+ )
1025
+ return retry_function(*args, **kwargs)
1026
+
1027
+ elif err.resp.status in [403]:
1028
+ self.hit_rate_limit(
1029
+ 30.0 + 10.0 * random.random(), multiplier=multiplier
1030
+ )
1031
+ return retry_function(*args, **kwargs)
1032
+ if err.resp.status in [500, 104]:
1033
+ self.hit_rate_limit(
1034
+ 30.0 + 10.0 * random.random(), multiplier=multiplier
1035
+ )
1036
+ return retry_function(*args, **kwargs)
1037
+ else:
1038
+ self.error(err, "Google API Error")
1039
+
1040
+ except pydrive2.files.ApiRequestError as err:
1041
+ # If the error is a rate limit or connection error,
1042
+ # wait and try again.
1043
+
1044
+ # Close connection to existing file
1045
+ if file is not None and file.http is not None:
1046
+ for c in file.http.connections.values():
1047
+ c.close()
1048
+
1049
+ if "code" in err.error and int(err.error["code"]) in [
1050
+ 403,
1051
+ 429,
1052
+ 500,
1053
+ 104,
1054
+ ]:
1055
+ self.hit_rate_limit(multiplier=multiplier)
1056
+ return retry_function(*args, **kwargs)
1057
+ else:
1058
+ self.error(err, "Google API Error")
1059
+
1060
+ except Exception as e:
1061
+ self.error(e)
1062
+
1063
+ # Close connection to existing file
1064
+ finally:
1065
+ if file is not None and file.http is not None:
1066
+ for c in file.http.connections.values():
1067
+ c.close()
1068
+
1069
+ return None
1070
+
1071
+ def cache_directory(self, folder_id):
1072
+ """quick way to get contents of directory"""
1073
+
1074
+ if folder_id in self._hot_directories:
1075
+ try:
1076
+ return self._hot_directories[folder_id]
1077
+ except Exception:
1078
+ pass
1079
+
1080
+ out = list([item for item in self.all_in_folder(folder_id)])
1081
+ self._hot_directories[folder_id] = out
1082
+ return out
1083
+
1084
+ @property
1085
+ def is_syncable(self):
1086
+ if (
1087
+ self.filepath_root is not None
1088
+ and self.sync_root is not None
1089
+ and self.shared_drive is not None
1090
+ ):
1091
+ return True
1092
+ return False
1093
+
1094
+ def initalize_google_drive_root(self):
1095
+ # BUG: This can creates duplicates
1096
+ self.info("initalizing engforge on gdrive")
1097
+
1098
+ # Only do this when we're in our main shared drive
1099
+ assert self.shared_drive == self.default_shared_drive
1100
+
1101
+ parent_id = self.sync_root_id
1102
+ for sfol, sref in STANDARD_FOLDERS.items():
1103
+ fol = self.get_or_create_folder(sfol, parent_id=parent_id)
1104
+ if "client" in sfol.lower():
1105
+ for clientname in engforge_clients():
1106
+ if clientname.lower() != "archive":
1107
+ self.get_or_create_folder(clientname, parent_id=fol["id"])
1108
+
1109
+ # Sync Methods
1110
+ def generate_sync_filepath_pairs(self, skip_existing=True):
1111
+ """Generates pairs of local and gdrive paths based on the filepath root
1112
+
1113
+ This doesn't include paths that contain directories with `.` or `_` in the path, or any directories with
1114
+ a `.skip_gsync` file included in it.
1115
+
1116
+ :return: tuple - (filepath, gdrivepath)
1117
+ """
1118
+ skipped_paths = []
1119
+
1120
+ parent_id = self.target_folder_id
1121
+ self.cache_directory(parent_id)
1122
+
1123
+ for i, (dirpath, dirnames, dirfiles) in enumerate(os.walk(self.filepath_root)):
1124
+ # we will modify dirnames in place to eliminate options for walking
1125
+ self.debug("looping through directory {}".format(dirpath))
1126
+ # Handle File Sync Ignores
1127
+ hidden = [
1128
+ pth.startswith(".") or pth.startswith("_")
1129
+ for pth in dirpath.split(os.sep)
1130
+ ]
1131
+ any_hidden = any(hidden)
1132
+ if any_hidden:
1133
+ for item in hidden:
1134
+ if item in dirnames:
1135
+ dirnames.pop(dirnames.index(item))
1136
+
1137
+ continue
1138
+
1139
+ if ".skip_gsync" in dirfiles or ".skip_gsync" in dirnames:
1140
+ # self.debug('skipping {}'.format(dirpath))
1141
+ dirnames = [] # just completely over it
1142
+ skipped_paths.append(dirpath)
1143
+ continue
1144
+
1145
+ if any(
1146
+ [
1147
+ os.path.commonpath([dirpath, spath]) == os.path.commonpath([spath])
1148
+ for spath in skipped_paths
1149
+ ]
1150
+ ):
1151
+ dirnames = [] # just completely over it
1152
+ # self.debug('skipping {}'.format(dirpath))
1153
+ continue
1154
+
1155
+ self.info(f"checking files in {dirpath}")
1156
+ gfypathfile = lambda fil: self.sync_path(
1157
+ os.path.realpath(os.path.join(dirpath, fil))
1158
+ )
1159
+
1160
+ file_paths = self.file_paths
1161
+ # Check if they already exist and we're skipping existing
1162
+
1163
+ if (
1164
+ not all([gfypathfile(fil) in file_paths for fil in dirfiles])
1165
+ or not skip_existing
1166
+ ):
1167
+ for fil in dirfiles:
1168
+ self.debug("checking file {}".format(fil))
1169
+ filpath = os.path.realpath(os.path.join(dirpath, fil))
1170
+ gdrive_path = gfypathfile(fil)
1171
+
1172
+ if (
1173
+ gdrive_path in self.file_paths and skip_existing
1174
+ ): # update file paths if work to be done
1175
+ self.debug("skipping {}".format(gdrive_path))
1176
+ continue
1177
+
1178
+ self.debug("new file to create! {}".format(gdrive_path))
1179
+ yield filpath, gdrive_path
1180
+
1181
+ def sync(self, force=False):
1182
+ """
1183
+ #1) Map Shared Drive Completely
1184
+ #2) Check and handle Duplicates
1185
+ #3) Get list of file pairs in the current file system and elimitate existing
1186
+ #4) Upload only the new files
1187
+ """
1188
+ try:
1189
+ if self.is_syncable:
1190
+ self.info(
1191
+ "syncing {} to client folder: {}".format(
1192
+ self.filepath_root, self.sync_root
1193
+ )
1194
+ )
1195
+
1196
+ duplicates = self.identify_duplicates()
1197
+ if duplicates:
1198
+ self.warning(f"found duplicates {duplicates}")
1199
+
1200
+ if not self.use_threadpool:
1201
+ self.debug("SYNCING WITH SINGLE THREAD...")
1202
+ for lpath, gpath in self.generate_sync_filepath_pairs(
1203
+ skip_existing=not force
1204
+ ):
1205
+ if gpath not in self.item_paths or force:
1206
+ dirname = os.path.dirname(gpath)
1207
+ if dirname not in self.item_paths:
1208
+ parent_id = self.ensure_g_path_get_id(
1209
+ dirname
1210
+ ) # Prevent duplicates!!!
1211
+ else:
1212
+ parent_id = self.get_gpath_id(dirname)
1213
+
1214
+ self.sync_path_pair_single(lpath, gpath)
1215
+
1216
+ else:
1217
+ if gpath in self.item_paths:
1218
+ self.debug("found existing {}".format(gpath))
1219
+
1220
+ else:
1221
+ self.debug("SYNCING WITH THREADPOOL...")
1222
+ self.thread_time_multiplier = 1.0 # ensure its normal
1223
+ # #Conventional way make folders if they don't exist
1224
+ submitted_set = set()
1225
+ with ThreadPoolExecutor(max_workers=self.num_threads) as pool:
1226
+ for lpath, gpath in self.generate_sync_filepath_pairs(
1227
+ skip_existing=not force
1228
+ ):
1229
+ dirname = os.path.dirname(gpath)
1230
+
1231
+ if dirname not in self.item_paths:
1232
+ parent_id = self.ensure_g_path_get_id(
1233
+ dirname
1234
+ ) # Prevent duplicates, by holding threads!!!
1235
+ else:
1236
+ parent_id = self.get_gpath_id(dirname)
1237
+
1238
+ if (
1239
+ any((gpath not in self.item_paths, force))
1240
+ and gpath not in submitted_set
1241
+ ):
1242
+ submitted_set.add(gpath)
1243
+ pool.submit(
1244
+ self.sync_path_pair_thread,
1245
+ lpath,
1246
+ gpath,
1247
+ parent_id,
1248
+ )
1249
+ else:
1250
+ if gpath in self.item_paths:
1251
+ self.debug("found existing {}".format(gpath))
1252
+
1253
+ self.thread_time_multiplier = 1.0
1254
+
1255
+ except Exception as e:
1256
+ self.error(e, "ISSUE SYNCING!")
1257
+
1258
+ else:
1259
+ self.info("SYNCING COMPLETE!")
1260
+
1261
+ def sync_path_pair_single(self, filepath, syncpath):
1262
+ try:
1263
+ gdirpath = os.path.dirname(syncpath)
1264
+ par_id = self.ensure_g_path_get_id(gdirpath)
1265
+
1266
+ if syncpath in self.item_paths:
1267
+ self.debug(f"updating {filepath} -> {syncpath}")
1268
+ fid = self.get_gpath_id(syncpath)
1269
+ self.upload_or_update_file(par_id, file_path=filepath, file_id=fid)
1270
+ self.cache_directory(par_id)
1271
+ else:
1272
+ self.debug(f"uploading {filepath} -> {syncpath}")
1273
+ self.upload_or_update_file(par_id, file_path=filepath)
1274
+ self.cache_directory(par_id)
1275
+
1276
+ except Exception as e:
1277
+ self.error(e, "Error Syncing Path ")
1278
+
1279
+ def sync_path_pair_thread(self, filepath, syncpath, parent_id=None):
1280
+ try:
1281
+ if syncpath in self.item_paths:
1282
+ self.debug(f"updating {filepath} -> {syncpath}")
1283
+ fid = self.get_gpath_id(syncpath)
1284
+ self.upload_or_update_file(parent_id, file_path=filepath, file_id=fid)
1285
+ self.cache_directory(parent_id)
1286
+ else:
1287
+ self.debug(f"uploading {filepath} -> {syncpath}")
1288
+ self.upload_or_update_file(parent_id, file_path=filepath)
1289
+ self.cache_directory(parent_id)
1290
+ except Exception as e:
1291
+ self.error(e, "Error Syncing Path ")
1292
+
1293
+ # Low Level Drive Interaction Handiling
1294
+ def create_file(self, input_args, file_path=None, content=None):
1295
+ """A wrapper for creating a folder with CreateFile, we check for rate limits ect"""
1296
+
1297
+ if "kind" not in input_args:
1298
+ input_args["kind"] = "drive#fileLink"
1299
+
1300
+ if "teamDriveId" not in input_args:
1301
+ input_args["teamDriveId"] = self.sync_root_id
1302
+
1303
+ self.debug(f"creating file w/ args: {input_args}")
1304
+
1305
+ self.sleep()
1306
+ file = self.gdrive.CreateFile(input_args)
1307
+ self.ensure_item_http(file)
1308
+ self.sleep()
1309
+
1310
+ with self.rate_limit_manager(
1311
+ self.create_file, 2, input_args, file_path, content
1312
+ ):
1313
+ if not self.dry_run and any((file_path is not None, content is not None)):
1314
+ action = "creating" if "id" not in input_args else "updating"
1315
+
1316
+ if content is not None:
1317
+ gfile_path = (
1318
+ file_path # Direct conversion, we have to input correctly
1319
+ )
1320
+ if isinstance(content, (str, unicode)):
1321
+ self.debug(
1322
+ f"{action} file with content string {input_args} -> {gfile_path}"
1323
+ )
1324
+ file.SetContentString(content)
1325
+ self.sleep()
1326
+ else:
1327
+ self.debug(
1328
+ f"{action} file with content bytes {input_args} -> {gfile_path}"
1329
+ )
1330
+ file.content = content
1331
+
1332
+ elif file_path is not None:
1333
+ file_path = os.path.realpath(file_path)
1334
+ file_name = os.path.basename(file_path)
1335
+ gfile_path = self.sync_path(file_path)
1336
+
1337
+ self.debug(f"{action} file with args {input_args} -> {gfile_path}")
1338
+ file.SetContentFile(file_path)
1339
+ self.sleep()
1340
+
1341
+ file.Upload(param={"supportsTeamDrives": True}) # Upload the file.
1342
+ self.sleep()
1343
+ file.FetchMetadata(fields="permissions,labels,mimeType")
1344
+ self.sleep()
1345
+
1346
+ self.info(f"uploaded {file_path}")
1347
+ self.cache_item(file)
1348
+
1349
+ elif self.dry_run:
1350
+ pass
1351
+ # TODO: Create fake folder item and cache it
1352
+
1353
+ if file.http is not None:
1354
+ for c in file.http.connections.values():
1355
+ c.close()
1356
+
1357
+ self.sleep()
1358
+ return file
1359
+
1360
+ def upload_or_update_file(
1361
+ self, parent_id, file_path=None, content=None, file_id=None, **kwargs
1362
+ ):
1363
+ """
1364
+ You need file_path or content and (g)file_path
1365
+ :param file_path: the absolute file path directory, the g-path will be deteremined, use as file title with content vs g filepath when provided alone
1366
+ :param parent_id: this is the id of the parent directory, we check that
1367
+ :param content: a string to create the file along with file name in file_path to create a file
1368
+ :param file_id: a direct reference to use when updating a file
1369
+ :return: a file meta object or None
1370
+ """
1371
+
1372
+ if content is None: # file path route
1373
+ file_path = os.path.realpath(file_path)
1374
+ file_name = os.path.basename(file_path)
1375
+ gfile_path = self.sync_path(file_path)
1376
+
1377
+ if file_id is not None: # Override takes precident
1378
+ self.debug(
1379
+ "updating id: {} {}->{}".format(file_id, parent_id, gfile_path)
1380
+ )
1381
+
1382
+ fil = self.create_file(
1383
+ {"id": file_id, "parents": [{"id": parent_id}]},
1384
+ file_path=file_path,
1385
+ )
1386
+ return fil
1387
+
1388
+ elif gfile_path in self.file_paths:
1389
+ self.debug("updating file {}->{}".format(parent_id, gfile_path))
1390
+
1391
+ fil = self.create_file(
1392
+ {
1393
+ "id": self.get_gpath_id(gfile_path),
1394
+ "parents": [{"id": parent_id}],
1395
+ },
1396
+ file_path=file_path,
1397
+ )
1398
+ return fil
1399
+
1400
+ else:
1401
+ self.debug("creating file {}->{}".format(file_path, gfile_path))
1402
+ fil = self.create_file(
1403
+ {"title": file_name, "parents": [{"id": parent_id}]},
1404
+ file_path=file_path,
1405
+ )
1406
+ return fil
1407
+
1408
+ else: # we use content
1409
+ assert (
1410
+ file_path is not None or file_id is not None
1411
+ ) # gotta have a title or existing reference
1412
+
1413
+ if file_id is not None:
1414
+ self.debug(
1415
+ "updating file w/ content id:{} par:{}".format(file_id, parent_id)
1416
+ )
1417
+ fil = self.create_file(
1418
+ {"id": file_id, "parents": [{"id": parent_id}]},
1419
+ file_path=file_path,
1420
+ content=content,
1421
+ ) # Add file_path for the caching portion
1422
+ return fil
1423
+
1424
+ elif file_path is not None:
1425
+ file_name = os.path.basename(file_path)
1426
+
1427
+ self.debug(
1428
+ "creating file w/ content {}->{}".format(parent_id, file_path)
1429
+ )
1430
+ fil = self.create_file(
1431
+ {"title": file_name, "parents": [{"id": parent_id}]},
1432
+ file_path=file_path,
1433
+ content=content,
1434
+ ) # Add file_path for the caching portion
1435
+ return fil
1436
+
1437
+ self.warning(f"could not create {file_path} in parent {parent_id}")
1438
+
1439
+ return None
1440
+
1441
+ def create_folder(self, input_args, upload=True):
1442
+ """A wrapper for creating a folder with CreateFile, we check for rate limits ect"""
1443
+ self.debug(f"creating Folder with args {input_args}")
1444
+
1445
+ if "mimeType" not in input_args:
1446
+ input_args["mimeType"] = "application/vnd.google-apps.folder"
1447
+
1448
+ file = self.gdrive.CreateFile(input_args)
1449
+ self.ensure_item_http(file)
1450
+ self.sleep()
1451
+
1452
+ with self.rate_limit_manager(
1453
+ self.create_folder, 2, input_args, upload, gfileMeta=file
1454
+ ):
1455
+ if upload and not self.dry_run:
1456
+ file.Upload(param={"supportsTeamDrives": True}) # Upload the file.
1457
+ self.sleep()
1458
+ self.debug(f"uploaded {input_args}")
1459
+ file.FetchMetadata(fields="permissions,labels,mimeType")
1460
+ self.sleep()
1461
+ elif self.dry_run:
1462
+ pass
1463
+ # TODO add fake data here with UUID
1464
+
1465
+ if file.http is not None:
1466
+ for c in file.http.connections.values():
1467
+ c.close()
1468
+
1469
+ self.sleep()
1470
+ return file
1471
+
1472
+ def get_or_create_folder(self, folder_name, parent_id=None, **kwargs):
1473
+ """Creates a folder in the parent folder if it doesn't already exist, otherwise return folder
1474
+ :param folder_name: the name of the folder to create
1475
+ :param parent_id: the id of the parent folder, if None will create in the parent directory
1476
+ :param override: bypass root, parent_id and folder name protections
1477
+ """
1478
+
1479
+ if parent_id is None:
1480
+ parent_id = self.target_folder_id
1481
+
1482
+ if "override" in kwargs:
1483
+ override = kwargs["override"]
1484
+ else:
1485
+ override = False
1486
+
1487
+ # Refresh parent
1488
+ self.cache_directory(parent_id)
1489
+ assert parent_id in self.item_nodes
1490
+
1491
+ parent_dir = self.item_nodes[parent_id]
1492
+ assert parent_dir.is_folder
1493
+
1494
+ # We only know the file
1495
+ parent_folders = [item.title for item in parent_dir.contents]
1496
+ if not len(parent_folders) == len(set(parent_folders)):
1497
+ self.warning(f"duplicate files found for parent {parent_id}")
1498
+
1499
+ if not self.is_ray_context:
1500
+ duplicates = {}
1501
+ for item in parent_dir.contents:
1502
+ if item.absolute_path not in duplicates:
1503
+ duplicates[item.absolute_path] = [item]
1504
+ else:
1505
+ duplicates[item.absolute_path].append(item)
1506
+ self.delete_duplicate_items(duplicates)
1507
+
1508
+ parent_items = {item.title: item for item in parent_dir.contents}
1509
+
1510
+ protect_name = not folder_name in self.protected_filenames
1511
+
1512
+ self.debug("found folder in parent {}: {}".format(parent_id, parent_folders))
1513
+
1514
+ if folder_name not in parent_folders: # Create It
1515
+ self.debug(f"creating {parent_id}->{folder_name}")
1516
+ fol = self.create_folder(
1517
+ {
1518
+ "title": folder_name,
1519
+ "parents": [{"id": parent_id}],
1520
+ "mimeType": "application/vnd.google-apps.folder",
1521
+ }
1522
+ )
1523
+ return self.cache_item(fol)
1524
+
1525
+ else: # Grab It
1526
+ self.debug("found folder {} in parent {}".format(folder_name, parent_id))
1527
+ return parent_items[folder_name]
1528
+
1529
+ # Duplicate Handiling
1530
+ def generate_file_paths(self):
1531
+ for node in self.nodes:
1532
+ if node._absolute_path is None:
1533
+ self.debug(f"generating filepath for {node.identity}")
1534
+ node.absolute_path
1535
+
1536
+ @property
1537
+ def duplicates_exist(self):
1538
+ """check if the number of paths is equal to the unique number of paths"""
1539
+ self.info("checking duplicates...")
1540
+ # TODO: Speed This Up, top down tree with parent caching, or try parallel processing.
1541
+ self.generate_file_paths()
1542
+ paths = self.file_paths
1543
+ spaths = set(paths)
1544
+ duplicates_exist = not (len(paths) == len(spaths))
1545
+ if duplicates_exist:
1546
+ self.info("DUPLICATES EXIST!!")
1547
+ else:
1548
+ self.info("no duplicates found :)")
1549
+ return duplicates_exist
1550
+
1551
+ def identify_duplicates(self):
1552
+ """we want to get a list of items which are duplicates, this doesnt determine if we should keep them"""
1553
+ items = set()
1554
+ duplicates = {}
1555
+ for node in self.item_nodes.values():
1556
+ abspath = node.absolute_path
1557
+ if abspath in items:
1558
+ self.debug(f"Duplicate {abspath}")
1559
+ duplicates[node.id] = node
1560
+ else:
1561
+ items.add(abspath)
1562
+ return duplicates
1563
+
1564
+ def group_duplicate_pairs(self, duplicate_paths_identified):
1565
+ """Groups sets of items per our intended unique path, we'll later select which to keep and which to remove"""
1566
+
1567
+ self.info("checking for duplicates...")
1568
+ duplicates_fols = {} # path: [it1,it2...]
1569
+ duplicates_fil_sets = {} # path: [it1,it2...]
1570
+
1571
+ if duplicate_paths_identified:
1572
+ self.info(f"duplicate paths identified {len(duplicate_paths_identified)}")
1573
+
1574
+ files, folders = {}, {}
1575
+ for mtch_id, mtch in duplicate_paths_identified.items():
1576
+ if mtch.is_folder:
1577
+ folders[mtch_id] = mtch
1578
+ elif mtch.is_file:
1579
+ files[mtch_id] = mtch
1580
+
1581
+ # Get duplicate folder items
1582
+ dup_paths = []
1583
+ for fid, fol in folders.items():
1584
+ dup_paths.append(fol.absolute_path)
1585
+
1586
+ for iid, fil in self.folder_nodes.items():
1587
+ if any([folpath == fil.absolute_path for folpath in dup_paths]):
1588
+ if fil.absolute_path in duplicates_fols:
1589
+ duplicates_fols[fil.absolute_path].append(fil)
1590
+ else:
1591
+ duplicates_fols[fil.absolute_path] = [fil]
1592
+
1593
+ # This identifies which items exist not in duplicate folders
1594
+ file_duplicates = {
1595
+ mtch_id: mtch
1596
+ for mtch_id, mtch in files.items()
1597
+ if not any(
1598
+ [
1599
+ self.path_contains(fol.absolute_path, mtch.absolute_path)
1600
+ for fid, fol in folders.items()
1601
+ ]
1602
+ )
1603
+ }
1604
+
1605
+ self.info(f"num file issues: {len(file_duplicates)}")
1606
+
1607
+ # Find items that exist in identified duplicate folders
1608
+ # Unessicary since we can just use the graph to determine these!
1609
+ # duplicates_fol_items = {} #path: [it1,it2...]
1610
+
1611
+ # for iid, fil in self.file_nodes.items():
1612
+ # if any([ self.path_contains(folpath, fil.absolute_path) for folpath in dup_paths]):
1613
+ # if fil.absolute_path in duplicates_fol_items:
1614
+ # duplicates_fol_items[fil.absolute_path].append(fil)
1615
+ # else:
1616
+ # duplicates_fol_items[fil.absolute_path] = [fil]
1617
+
1618
+ # Find items that exist only as pure duplicates
1619
+
1620
+ oked_paths = set()
1621
+
1622
+ duplicate_file_paths = set(
1623
+ [sfil.absolute_path for sid, sfil in file_duplicates.items()]
1624
+ )
1625
+ for iid, fil in self.file_nodes.items():
1626
+ if fil.absolute_path in oked_paths:
1627
+ continue
1628
+ if fil.absolute_path in duplicates_fil_sets:
1629
+ duplicates_fil_sets[fil.absolute_path].append(fil)
1630
+ else:
1631
+ if fil.absolute_path in duplicate_file_paths:
1632
+ duplicates_fil_sets[fil.absolute_path] = [fil]
1633
+ else:
1634
+ oked_paths.add(fil.absolute_path)
1635
+
1636
+ self.info(f"duplicate files paths to fix: {len(duplicate_file_paths)}")
1637
+
1638
+ check_fol_fil_intersection = {
1639
+ path: mtch
1640
+ for path, mtch in duplicates_fil_sets.items()
1641
+ if any([self.path_contains(folpath, path) for folpath in dup_paths])
1642
+ }
1643
+
1644
+ assert len(check_fol_fil_intersection) == 0
1645
+ self.info(f"looks ok, proceeding")
1646
+
1647
+ return duplicates_fols, duplicates_fil_sets
1648
+
1649
+ def remove_duplicates(self):
1650
+ self.info("REMOVING DUPLICATES!!")
1651
+
1652
+ self.sleep(2)
1653
+
1654
+ duplicate_paths_identified = self.identify_duplicates()
1655
+
1656
+ duplicates_fols, duplicates_fil_sets = self.group_duplicate_pairs(
1657
+ duplicate_paths_identified
1658
+ )
1659
+ # Proceed with work
1660
+ self.process_duplicates(duplicates_fols, duplicates_fil_sets)
1661
+
1662
+ def process_duplicates(self, duplicate_folder_groups, duplicate_file_groups):
1663
+ """evals a set of duplicates and adjusts the fileystem as nessicary, the files and folders input should have no interaction"""
1664
+
1665
+ self.delete_duplicate_items(duplicate_file_groups)
1666
+ self.delete_duplicate_folders(duplicate_folder_groups)
1667
+
1668
+ def delete_duplicate_items(self, item_dict):
1669
+ self.info(f"DELETING {len(item_dict)} ITEMS")
1670
+
1671
+ pool = None
1672
+ if self.use_threadpool and self.num_threads > 1:
1673
+ pool = ThreadPoolExecutor(max_workers=self.num_threads)
1674
+
1675
+ try:
1676
+ # First we want to delete the duplicate files since thats easy
1677
+ for fpath, items in item_dict.items():
1678
+ group = [
1679
+ {"item": item, "new": days_since_2020(item)}
1680
+ for i, item in enumerate(items)
1681
+ ]
1682
+ order_items = sorted(group, key=lambda itd: itd["new"])
1683
+ keep = order_items[-1] # This will be the newest!
1684
+ discard = order_items[:-1]
1685
+
1686
+ self.debug(f"keeping {keep}, removing {discard} for {fpath}")
1687
+ for remove in discard:
1688
+ item = remove["item"]
1689
+ # if pool is not None:
1690
+ # pool.submit(item.delete) #make thread safe
1691
+ # else:
1692
+ item.delete()
1693
+
1694
+ except Exception as e:
1695
+ self.error(e, "Issue deleting duplicates")
1696
+ finally:
1697
+ if pool is not None:
1698
+ pool.shutdown()
1699
+
1700
+ def delete_duplicate_folders(self, item_dict):
1701
+ def score_folders_suggest_action(items):
1702
+ """Score size x newness by days from jan1st 2020 if content, otherwise just by date"""
1703
+ assert all([it.is_folder for it in items])
1704
+
1705
+ all_empty = all([content_size(it) == 0 for it in items])
1706
+ if all_empty: # Score on date
1707
+ group = [
1708
+ {
1709
+ "it": item,
1710
+ "new": days_since_2020(item),
1711
+ "size": content_size(item),
1712
+ "score": days_since_2020(item),
1713
+ }
1714
+ for i, item in enumerate(items)
1715
+ ]
1716
+ else:
1717
+ group = [
1718
+ {
1719
+ "it": item,
1720
+ "new": days_since_2020(item),
1721
+ "size": content_size(item),
1722
+ "score": content_size(item) * days_since_2020(item),
1723
+ }
1724
+ for i, item in enumerate(items)
1725
+ ]
1726
+
1727
+ # Sort by highest score
1728
+ order_items = sorted(group, key=lambda itd: itd["score"], reverse=True)
1729
+ keep = order_items[0] # keep oldest folder
1730
+ remove = order_items[1:]
1731
+ out = {"keep": keep, "remove": remove}
1732
+ self.info(f"suggested work: {out}")
1733
+ return out
1734
+
1735
+ for pth, items in item_dict.items():
1736
+ work_order = score_folders_suggest_action(items)
1737
+ removals = work_order["remove"]
1738
+ target = work_order["keep"]
1739
+
1740
+ target_contents = target["it"].contents
1741
+
1742
+ for remove in removals:
1743
+ duplicate_folder = remove["it"]
1744
+ if remove["size"] == 0:
1745
+ duplicate_folder.delete()
1746
+ else:
1747
+ remove_contents = duplicate_folder.contents
1748
+ self.info(
1749
+ f"found a duplicate folder with contents {remove_contents}"
1750
+ )
1751
+ pass
1752
+
1753
+ # TODO: change parent for items in this folder
1754
+ # for item in this folder, change the parent to the target folder after checking if its new than the other folder's contents.
1755
+ # First prep the target folder by deleting any duplicates.
1756
+ # finally delete this folder
1757
+
1758
+ # MAKE SURE FILESYSTEM IS CORRECTED UPDATED AS GOOGLE IS, removing the folder reference after adding the other parent should work
1759
+
1760
+ # Id and path relationships
1761
+ def ensure_g_path_get_id(self, gpath):
1762
+ """walks these internal google paths ensuring everythign is created from root
1763
+ This one is good for a cold entry of a path
1764
+
1765
+ This method can create duplicates when used in parallel contexts"""
1766
+ if gpath.startswith(self.shared_drive):
1767
+ gpath = gpath.replace(
1768
+ self.shared_drive, ""
1769
+ ) # gpath no have groot, we later add it into the path
1770
+
1771
+ if gpath.startswith("/"):
1772
+ gpath = gpath.replace("/", "", 1)
1773
+
1774
+ if ".." in gpath or "mnt" in gpath: # guard on wsl
1775
+ return self.sync_root_id
1776
+
1777
+ # if gpath == self.shared_drive:
1778
+ # return self.sync_root_id
1779
+
1780
+ self.debug("ensuring path {}".format(gpath))
1781
+ if self.target_folder_id is not None:
1782
+ parent_id = self.target_folder_id
1783
+ else:
1784
+ parent_id = self.sync_root_id
1785
+
1786
+ if gpath in self.folder_paths:
1787
+ parent_id = self.get_gpath_id(gpath)
1788
+ self.cache_directory(parent_id)
1789
+ return parent_id
1790
+
1791
+ current_pos = self.shared_drive
1792
+ for sub in gpath.split(os.sep):
1793
+ if sub == "..":
1794
+ continue
1795
+
1796
+ if sub != "" and sub != "root":
1797
+ current_pos = os.path.join(current_pos, sub)
1798
+ self.debug(f"ensure-path: {current_pos}")
1799
+ if current_pos in self.folder_paths:
1800
+ parent_id = self.get_gpath_id(current_pos)
1801
+
1802
+ self.debug(
1803
+ f"ensure-path: grabing existing path {current_pos}:{parent_id}"
1804
+ )
1805
+ self.cache_directory(parent_id)
1806
+
1807
+ else:
1808
+ self.debug(f"path doesnt exist, create it {current_pos}")
1809
+ if parent_id is not None and not any(
1810
+ [fol.startswith(".") for fol in current_pos.split(os.sep)]
1811
+ ):
1812
+ fol = self.get_or_create_folder(sub, parent_id)
1813
+ if fol is not None:
1814
+ parent_id = fol.id
1815
+ self.cache_directory(parent_id)
1816
+ else:
1817
+ parent_id = None
1818
+
1819
+ if parent_id is not None:
1820
+ return parent_id
1821
+
1822
+ elif not any([fol.startswith(".") for fol in current_pos.split(os.sep)]):
1823
+ self.warning(f"Failed To get Gpath {gpath} Trying again ")
1824
+ return self.ensure_g_path_get_id(gpath)
1825
+
1826
+ return self.root_id
1827
+
1828
+ def get_gpath_id(self, gpath):
1829
+ """ensure only one match is found, duplicates are delt with"""
1830
+
1831
+ fids, paths = list(zip(*self.item_cache.items()))
1832
+ fids, paths = numpy.array(fids), numpy.array(paths)
1833
+
1834
+ matches = list(fids[paths == gpath])
1835
+ if len(matches) > 1:
1836
+ self.warning(f"gpathid found matches {matches} for {gpath}, using first")
1837
+ # TODO: Handle this case!
1838
+ nodes = [self.item_nodes[mtch] for mtch in matches]
1839
+ snodes = sorted(nodes, key=lambda it: days_since_2020(it), reverse=True)
1840
+ if snodes[0].is_folder:
1841
+ return snodes[0].id # This returns the oldest folder
1842
+
1843
+ elif snodes[-1].is_file:
1844
+ return snodes[-1].id # This returns the newest file
1845
+
1846
+ return snodes[0].id # This returns the oldest folder
1847
+
1848
+ elif len(matches) == 1:
1849
+ return self.item_nodes[matches[0]].id
1850
+
1851
+ self.warning(f"found no match, ensuring path")
1852
+ return self.ensure_g_path_get_id(gpath)
1853
+
1854
+ def get_gpath_matches(self, gpath):
1855
+ """ensure only one match is found, duplicates are delt with"""
1856
+
1857
+ fids, paths = list(zip(*self.item_cache.items()))
1858
+ fids, paths = numpy.array(fids), numpy.array(paths)
1859
+
1860
+ matches = list(fids[paths == gpath])
1861
+ if len(matches) > 1:
1862
+ self.warning(f"found matches {matches} for {gpath}, using first")
1863
+ # TODO: Handle this case!
1864
+ return [self.item_nodes[mtc] for mtc in matches]
1865
+
1866
+ elif len(matches) == 1:
1867
+ return self.item_nodes[matches[0]]
1868
+
1869
+ self.warning(f"found no match, ensuring path")
1870
+ return self.ensure_g_path_get_id(gpath)
1871
+
1872
+ # Meta Based Caching
1873
+ def close_item_http(self, item):
1874
+ if item.http is not None:
1875
+ for c in item.http.connections.values():
1876
+ c.close()
1877
+
1878
+ def ensure_item_http(self, item):
1879
+ """to use multi-threads we must close connections"""
1880
+ if item.http is not None:
1881
+ self.close_item_http(item)
1882
+ item.http = self.gauth.Get_Http_Object()
1883
+ else:
1884
+ item.http = self.gauth.Get_Http_Object()
1885
+
1886
+ @property
1887
+ def nodes(self):
1888
+ return list(self._filesystem.nodes())
1889
+
1890
+ @property
1891
+ def item_nodes(self):
1892
+ with self.filesystem as fs:
1893
+ return {node.id: node for node in fs.nodes()}
1894
+
1895
+ @property
1896
+ def item_cache(self):
1897
+ with self.filesystem as fs:
1898
+ return {node.id: node.absolute_path for node in fs.nodes()}
1899
+
1900
+ @property
1901
+ def item_paths(self):
1902
+ with self.filesystem as fs:
1903
+ return [node.absolute_path for node in fs.nodes()]
1904
+
1905
+ @property
1906
+ def file_nodes(self):
1907
+ with self.filesystem as fs:
1908
+ return {node.id: node for node in fs.nodes() if node.is_file}
1909
+
1910
+ @property
1911
+ def file_cache(self):
1912
+ with self.filesystem as fs:
1913
+ return {node.id: node.absolute_path for node in fs.nodes() if node.is_file}
1914
+
1915
+ @property
1916
+ def file_paths(self):
1917
+ with self.filesystem as fs:
1918
+ return [node.absolute_path for node in fs.nodes() if node.is_file]
1919
+
1920
+ @property
1921
+ def folder_nodes(self):
1922
+ with self.filesystem as fs:
1923
+ return {node.id: node for node in fs.nodes() if node.is_folder}
1924
+
1925
+ @property
1926
+ def folder_cache(self):
1927
+ with self.filesystem as fs:
1928
+ return {
1929
+ node.id: node.absolute_path for node in fs.nodes() if node.is_folder
1930
+ }
1931
+
1932
+ @property
1933
+ def folder_paths(self):
1934
+ with self.filesystem as fs:
1935
+ return [node.absolute_path for node in fs.nodes() if node.is_folder]
1936
+
1937
+ def addFileNode(self, node):
1938
+ if node.id not in self.item_nodes: # Don't do it again!
1939
+ if node.is_file:
1940
+ self.msg(f"adding file {node}")
1941
+ elif node.is_folder:
1942
+ self.debug(f"adding folder {node}")
1943
+
1944
+ with self.filesystem as fs:
1945
+ # Add items to network
1946
+ fs.add_node(node, **node.attributes)
1947
+ # Assign parent relationships
1948
+ for parent in node.listed_parents:
1949
+ fs.add_edge(parent["id"], node.id)
1950
+
1951
+ def removeNode(self, node):
1952
+ self.debug(f"removing node {node}")
1953
+ if node.id in self.item_nodes:
1954
+ with self.filesystem as fs:
1955
+ # Add items to network
1956
+ fs.remove_node(node)
1957
+
1958
+ if node.id in self.protected_ids:
1959
+ self.protected_ids.remove(node.id)
1960
+
1961
+ def cache_item(self, item_meta):
1962
+ if "teamDriveId" in item_meta and item_meta["teamDriveId"] == self.sync_root_id:
1963
+ if item_meta["id"] not in self.item_nodes:
1964
+ node = FileNode(self, item_meta)
1965
+ self.addFileNode(node)
1966
+ return node
1967
+ else:
1968
+ return self.item_nodes[item_meta["id"]]
1969
+
1970
+ def path_contains(self, rootpath, checkpath):
1971
+ return os.path.commonpath([rootpath]) == os.path.commonpath(
1972
+ [rootpath, checkpath]
1973
+ )
1974
+
1975
+ def in_client_folder(self, local_file_path):
1976
+ return os.path.commonpath([self.filepath_root]) == os.path.commonpath(
1977
+ [file_path, self.filepath_root]
1978
+ )
1979
+
1980
+ def search_items(self, q="", parent_id=None, **kwargs):
1981
+ """A wrapper for `ListFile` that manages exceptions"""
1982
+ if "in_trash" in kwargs:
1983
+ q += " and trashed=true"
1984
+ else:
1985
+ if "trashed" not in q:
1986
+ q += " and trashed=false"
1987
+
1988
+ self.debug(f"search: pid:{parent_id} q:{q}")
1989
+ success = False
1990
+
1991
+ input_args = {
1992
+ "q": q,
1993
+ "supportsAllDrives": True,
1994
+ "includeItemsFromAllDrives": True,
1995
+ "maxResults": 100,
1996
+ }
1997
+ for key in input_args.keys():
1998
+ if key in kwargs:
1999
+ kwargs.pop(key)
2000
+ input_args.update(kwargs)
2001
+
2002
+ if parent_id in self.item_nodes:
2003
+ parent = self.item_nodes[parent_id]
2004
+ existing_contents = {n.id: n for n in parent.contents}
2005
+ else:
2006
+ existing_contents = {}
2007
+
2008
+ success = False
2009
+ with self.rate_limit_manager(self.search_items, 2, q, parent_id, **kwargs):
2010
+ self.sleep()
2011
+ for output in self.gdrive.ListFile(input_args):
2012
+ for file in output:
2013
+ filenode = self.cache_item(file)
2014
+ if filenode.id in existing_contents:
2015
+ existing_contents.pop(
2016
+ filenode.id
2017
+ ) # Ones not observed will remain for later
2018
+ yield filenode
2019
+ self.sleep()
2020
+ success = True
2021
+
2022
+ if existing_contents and success:
2023
+ # BUG: 3rd party removal not working, stale references, maybe a refresh method?
2024
+ self.debug("some expected items werent found remove them:")
2025
+ for nid, node in existing_contents.items():
2026
+ self.removeNode(node)
2027
+
2028
+ def files_in_folder(self, folder_id=None):
2029
+ if folder_id is None:
2030
+ folder_id = self.sync_root_id
2031
+
2032
+ self.debug(f"searching {folder_id} for files")
2033
+ output = self.search_items(
2034
+ f"'{folder_id}' in parents and trashed=false", folder_id
2035
+ )
2036
+ for file in output:
2037
+ if file.is_file:
2038
+ yield file
2039
+
2040
+ def folders_in_folder(self, folder_id=None):
2041
+ if folder_id is None:
2042
+ folder_id = self.sync_root_id
2043
+
2044
+ self.debug(f"searching {folder_id} for folders")
2045
+ output = self.search_items(
2046
+ f"'{folder_id}' in parents and trashed=false", folder_id
2047
+ )
2048
+ for file in output:
2049
+ if file.is_folder:
2050
+ yield file
2051
+
2052
+ def all_in_folder(self, folder_id=None):
2053
+ if folder_id is None:
2054
+ folder_id = self.sync_root_id
2055
+
2056
+ self.debug(f"searching {folder_id} for anything")
2057
+ output = list(
2058
+ self.search_items(f"'{folder_id}' in parents and trashed=false", folder_id)
2059
+ )
2060
+ for file in output:
2061
+ yield file
2062
+
2063
+ def dict_by_title(self, items_list):
2064
+ return {
2065
+ (it.title if isinstance(it, FileNode) else it["title"]): it
2066
+ for it in items_list
2067
+ }
2068
+
2069
+ def hit_rate_limit(self, sleep_time=5, multiplier=2):
2070
+ old_sleeptime = self._sleep_time
2071
+
2072
+ if not isinstance(multiplier, (int, float)):
2073
+ multiplier = 2.0
2074
+
2075
+ if self.use_threadpool:
2076
+ self._sleep_time = min(
2077
+ self._sleep_time * multiplier ** (1.0 / self.num_threads),
2078
+ self.max_sleep_time,
2079
+ )
2080
+ else:
2081
+ self._sleep_time = min(self._sleep_time * multiplier, self.max_sleep_time)
2082
+
2083
+ dynamicsleep = sleep_time * 0.5 + random.random() * sleep_time
2084
+ self.warning(
2085
+ f"sleepingx{self.num_threads} {dynamicsleep}s, change sleep {old_sleeptime} -> {self._sleep_time} then continuing"
2086
+ )
2087
+
2088
+ self.sleep(dynamicsleep)
2089
+
2090
+ def reset_sleep_time(self, new_sleep_time=0.5):
2091
+ self._sleep_time = new_sleep_time
2092
+
2093
+ def sleep(self, val=None):
2094
+ """a serious effort has gone into avoiding spamming rate limits.
2095
+
2096
+ This would be a great place for a ray actor"""
2097
+ self.call_count += 1.0
2098
+
2099
+ if isinstance(val, (float, int)):
2100
+ time.sleep(self._sleep_time + val) # ultra conservative
2101
+ return
2102
+
2103
+ main_thread = threading.main_thread()
2104
+
2105
+ # Decay sleep time
2106
+ if self._sleep_time > self.min_sleep_time:
2107
+ if self.use_threadpool:
2108
+ self._sleep_time = self._sleep_time * (
2109
+ 0.999 ** (1.0 / self.num_threads)
2110
+ ) # This normalizes decay
2111
+ else:
2112
+ self._sleep_time = self._sleep_time * 0.995
2113
+ else:
2114
+ self._sleep_time = max(self._sleep_time, self.min_sleep_time) * (
2115
+ 1.0 + random.random() * self.time_fuzz
2116
+ )
2117
+
2118
+ if not main_thread:
2119
+ time.sleep(
2120
+ min(
2121
+ self.thread_time_multiplier * self._sleep_time,
2122
+ self.max_sleep_time,
2123
+ )
2124
+ )
2125
+ else:
2126
+ time.sleep(self._sleep_time)
2127
+
2128
+ @property
2129
+ def min_sleep_time(self):
2130
+ # not going too fast is important since rate limits will result in incompete searches and PyDrive doesn't make those accessible
2131
+ reqpermin = 10000
2132
+ reqpersec = reqpermin / 60.0
2133
+ return max(self.num_threads * 3.0 / reqpersec, self._absolute_min_sleep)
2134
+
2135
+ def draw_filesystem(self, *args, **kwargs):
2136
+ nx.draw(self._filesystem, *args, **kwargs)
2137
+
2138
+ @property
2139
+ def is_ray_context(self):
2140
+ return False
2141
+
2142
+ # FIXME: not working on return
2143
+ try:
2144
+ rtc = get_runtime_context()
2145
+ if not rtc.worker.actor_id.is_nil():
2146
+ return True # in ray task
2147
+ except Exception as e:
2148
+ self.error(e, "issue checking ray sync context")
2149
+
2150
+ return False
2151
+
2152
+ @property
2153
+ def num_threads(self):
2154
+ # TODO: add a try fail to determine if in ray worker, if we are use 1
2155
+ if self.use_threadpool:
2156
+ if self.is_ray_context:
2157
+ return 1
2158
+ return self._max_num_threads
2159
+ return 1
2160
+
2161
+ # INPUT Methods
2162
+ @contextmanager
2163
+ def context(self, filepath_root, sync_root):
2164
+ old_filepath = self.filepath_root
2165
+ old_syncroot = self.sync_root
2166
+
2167
+ try:
2168
+ if self.filepath_root == filepath_root and self.sync_root == sync_root:
2169
+ self.debug(f"Alread in context {filepath_root} -> {sync_root}")
2170
+ yield self # no work to do
2171
+
2172
+ else:
2173
+ self.info(f"Changing Contexts {filepath_root}->{sync_root}")
2174
+ self.filepath_root = filepath_root
2175
+ self.sync_root = sync_root
2176
+ self.initalize(reinit=True)
2177
+ yield self
2178
+
2179
+ except Exception as e:
2180
+ self.error(e, f"Issue In Drive Context {filepath_root} -> {sync_root}")
2181
+
2182
+ finally:
2183
+ self.filepath_root = old_filepath
2184
+ self.sync_root = old_syncroot
2185
+
2186
+ @property
2187
+ def filepath_root(self):
2188
+ """This is the location on the filepath where the system will begin its sync"""
2189
+ return self._filepath_root
2190
+
2191
+ @filepath_root.setter
2192
+ def filepath_root(self, filepath_root):
2193
+ """Check the filepath exists and set it"""
2194
+ if self._filepath_root is not None:
2195
+ do_reinitalize = (
2196
+ True # We already initalize with previous input, do it again!
2197
+ )
2198
+ else:
2199
+ do_reinitalize = False
2200
+
2201
+ if filepath_root is not None:
2202
+ if os.path.exists(filepath_root):
2203
+ self._filepath_root = os.path.realpath(filepath_root)
2204
+ self.filepath_inferred = False
2205
+ self.debug(f"using verified filepath {self._filepath_root}")
2206
+ else:
2207
+ raise SourceFolderNotFound(f"could not find filepath {filepath_root}")
2208
+
2209
+ elif not self.explict_input_only and self.guess_sync_path: # Infer It
2210
+ if in_client_dir():
2211
+ if in_wsl():
2212
+ self._filepath_root = client_path(skip_wsl=False)
2213
+ else:
2214
+ self._filepath_root = current_diectory()
2215
+ self.filepath_inferred = True
2216
+ self.debug(f"using infered client path: {self._filepath_root}")
2217
+ elif (
2218
+ self._shared_drive == self.default_shared_drive and in_dropbox_dir()
2219
+ ): # OTTERSYNC
2220
+ if in_wsl():
2221
+ self._filepath_root = client_path(skip_wsl=False)
2222
+ else:
2223
+ self._filepath_root = client_path(skip_wsl=False)
2224
+ self.filepath_inferred = True
2225
+ self.debug(f"using infered engforge path: {self._filepath_root}")
2226
+
2227
+ if do_reinitalize:
2228
+ self.reset_target_id()
2229
+
2230
+ @property
2231
+ def shared_drive(self):
2232
+ """This is the shared drive to be used, ensures starting with ex: shared:DRIVENAME"""
2233
+ return self._shared_drive
2234
+
2235
+ @shared_drive.setter
2236
+ def shared_drive(self, shared_drive):
2237
+ """Check the share drive exists and set it, a shared: will prepend the drive name"""
2238
+ if shared_drive is not None:
2239
+ just_drive = shared_drive.replace("shared:", "")
2240
+ has_drives = self.shared_drives
2241
+ if just_drive in has_drives:
2242
+ self._shared_drive = f"shared:{just_drive}"
2243
+ self.debug(f"using input shared drive: {self._shared_drive}")
2244
+ else:
2245
+ raise SharedDriveNotFound(f"Could not find {shared_drive}")
2246
+ elif not self.explict_input_only: # Inferance
2247
+ creds_info = self.credentials_info
2248
+
2249
+ # 1) Look for credentials in service account creds
2250
+ if "shared_drive" in creds_info:
2251
+ self._shared_drive = creds_info["shared_drive"]
2252
+ self.debug(f"using service account shared drive: {self._shared_drive}")
2253
+
2254
+ # 2) Look for enviornmental variable
2255
+ elif "CLIENT_GDRIVE_PATH" in os.environ and os.environ[
2256
+ "CLIENT_GDRIVE_PATH"
2257
+ ].startswith("shared:"):
2258
+ gpath = os.environ["CLIENT_GDRIVE_PATH"]
2259
+ drive_canidate = None
2260
+ if "/" in gpath:
2261
+ sep = "/"
2262
+ drive_canidate = gpath.split(sep)[0]
2263
+ elif "\\" in gpath:
2264
+ sep = "\\"
2265
+ drive_canidate = gpath.split(sep)[0]
2266
+ else:
2267
+ drive_canidate = gpath
2268
+
2269
+ if (
2270
+ drive_canidate is not None and drive_canidate
2271
+ ): # should be a string not empty
2272
+ drive_canidate = drive_canidate.replace("shared:", "")
2273
+ if drive_canidate in self.shared_drives:
2274
+ self._shared_drive = f"shared:{drive_canidate}"
2275
+ self.debug(f"using env-var shared drive: {self._shared_drive}")
2276
+ else:
2277
+ raise SharedDriveNotFound(
2278
+ f"env var shared drive not found {gpath}->{drive_canidate}"
2279
+ )
2280
+ else:
2281
+ raise SharedDriveNotFound(
2282
+ f"env var for shared drive invalid {gpath}->{drive_canidate}"
2283
+ )
2284
+
2285
+ # 3) Use Default
2286
+ else:
2287
+ self._shared_drive = self.default_shared_drive
2288
+ self.debug(f"using default shared drive: {self._shared_drive}")
2289
+
2290
+ # Set the root_id after everything has settled
2291
+ if self._shared_drive is not None:
2292
+ direct_name = self._shared_drive.replace("shared:", "")
2293
+ if direct_name in self.shared_drives:
2294
+ self._sync_root_id = self.shared_drives[direct_name]
2295
+ return # otherwise we fail here
2296
+
2297
+ raise SharedDriveNotFound(
2298
+ f"shared drive identification failed, our current assignment {self._shared_drive}"
2299
+ )
2300
+
2301
+ @property
2302
+ def sync_root(self):
2303
+ """This is the relative drive path to google drive root of client ex. ClientFolder/Client1
2304
+ These values should not start with a sep `/` or a `shared:DRIVE` however they will be removed on setting
2305
+ """
2306
+ return self._sync_root
2307
+
2308
+ @property
2309
+ def full_sync_root(self):
2310
+ """A conveience method to get the shared drive full path to use with folder_cache"""
2311
+ return os.path.join(self.shared_drive, self.sync_root)
2312
+
2313
+ @sync_root.setter
2314
+ def sync_root(self, sync_root):
2315
+ self.info(f"Setting Sync Root '{sync_root}' ")
2316
+ if self._sync_root is not None and self._sync_root != sync_root:
2317
+ do_reinitalize = (
2318
+ True # We already initalize with previous input, do it again!
2319
+ )
2320
+ else:
2321
+ do_reinitalize = False
2322
+
2323
+ if sync_root is not None: # Use Input First
2324
+ # Do some formatting
2325
+ if sync_root.startswith("/"):
2326
+ sync_root = sync_root.replace("/", "", 1)
2327
+ if sync_root.startswith("\\"):
2328
+ sync_root = sync_root.replace("\\", "", 1)
2329
+ if sync_root.startswith("shared:"):
2330
+ if "/" in sync_root:
2331
+ sync_root = os.path.join("/", sync_root.split("/")[1:])
2332
+ if "\\" in sync_root:
2333
+ sync_root = os.path.join("\\", sync_root.split("\\")[1:])
2334
+ self._sync_root = sync_root
2335
+ self.debug(f"using input sync root {self._sync_root}")
2336
+
2337
+ elif not self.explict_input_only:
2338
+ creds_info = self.credentials_info
2339
+
2340
+ # 1) Look for credentials in service account creds
2341
+ if "shared_sync_path" in creds_info and creds_info["shared_sync_path"]:
2342
+ self._sync_root = creds_info["shared_sync_path"]
2343
+ self.debug(f"using service account sync path: {self._sync_root}")
2344
+
2345
+ # 2) Look for enviornmental variable
2346
+ elif (
2347
+ "CLIENT_GDRIVE_PATH" in os.environ
2348
+ and os.environ["CLIENT_GDRIVE_PATH"]
2349
+ and os.environ["CLIENT_GDRIVE_PATH"].startswith(self.shared_drive)
2350
+ and os.environ["CLIENT_GDRIVE_PATH"] != self.shared_drive
2351
+ ):
2352
+ gpath = os.environ["CLIENT_GDRIVE_PATH"]
2353
+ drive_canidate = None
2354
+ if (
2355
+ "shared:" in gpath
2356
+ ): # Git rid of first drive as thats the shared folder
2357
+ if "/" in gpath:
2358
+ sep = "/"
2359
+ drive_canidate = gpath.split(sep)[1:]
2360
+ elif "\\" in gpath:
2361
+ sep = "\\"
2362
+ drive_canidate = gpath.split(sep)[1:]
2363
+ else:
2364
+ drive_canidate = "" # root
2365
+ else:
2366
+ drive_canidate = gpath # one could only assume this is correct
2367
+
2368
+ self._sync_root = drive_canidate
2369
+ self.debug(f"using env-var sync path: {self._sync_root}")
2370
+
2371
+ # 3) We do our best
2372
+ elif self.guess_sync_path:
2373
+ if not self.filepath_inferred: # We gave a filepath
2374
+ guessfilepath = self.filepath_root
2375
+ else:
2376
+ guessfilepath = current_diectory()
2377
+
2378
+ if in_client_dir(
2379
+ guessfilepath
2380
+ ): # in a client directory, so map to ClientFolders or Relative Dir
2381
+ if self.shared_drive == self.default_shared_drive:
2382
+ # Map to ClientFolders/Client/matching/path...
2383
+ relpath = os.path.relpath(guessfilepath, engforge_projects())
2384
+ self._sync_root = os.path.join(self.default_sync_path, relpath)
2385
+ else:
2386
+ # Map to /matching/path... since client drive assumed root
2387
+ self._sync_root = os.path.relpath(
2388
+ guessfilepath, client_path(skip_wsl=False)
2389
+ )
2390
+ self.debug(
2391
+ f"client infered sync path from given filepath : {self._sync_root}"
2392
+ )
2393
+
2394
+ elif in_dropbox_dir(guessfilepath): # we're not in a client folder
2395
+ if self.shared_drive == self.default_shared_drive:
2396
+ # Map to Dropbox/whatever/path
2397
+ self._sync_root = os.path.relpath(
2398
+ guessfilepath, engforge_dropbox()
2399
+ )
2400
+ else:
2401
+ # Its Root!
2402
+ self._sync_root = ""
2403
+ self.debug(
2404
+ f"client infered sync path from given filepath : {self._sync_root}"
2405
+ )
2406
+ else:
2407
+ self._sync_root = ""
2408
+ self.debug(
2409
+ f"client infered sync path from given filepath : {self._sync_root}"
2410
+ )
2411
+
2412
+ if self._sync_root is None:
2413
+ raise SyncPathNotFound(f"No sync_path found for {self._sync_root}")
2414
+
2415
+ # if do_reinitalize:
2416
+ # self.info("Reinitalizing")
2417
+ # self.initalize()
2418
+
2419
+ @property
2420
+ def sync_root_id(self):
2421
+ return self._sync_root_id
2422
+
2423
+ @property
2424
+ def target_folder_id(self):
2425
+ return self._target_folder_id
2426
+
2427
+ # Creds Methods
2428
+ @property
2429
+ def creds_file(self):
2430
+ return self._creds_file
2431
+
2432
+ @creds_file.setter
2433
+ def creds_file(self, file_path):
2434
+ if os.path.exists(file_path):
2435
+ self._creds_file = file_path
2436
+ else:
2437
+ raise CredsNotFound(f"no credentials found in {file_path}")
2438
+
2439
+ @property
2440
+ def credentials_info(self):
2441
+ with open(self._creds_file, "r") as fp:
2442
+ out = json.loads(fp.read())
2443
+ if "private_key" in out:
2444
+ out.pop("private_key") # Security yay
2445
+ return out
2446
+
2447
+ # Magic Methods (Pickling here)
2448
+ def __getstate__(self):
2449
+ """Remove active connection objects, they are not picklable"""
2450
+ # self.debug('removing unpiclable info')
2451
+ d = self.__dict__.copy()
2452
+ d["gsheets"] = None
2453
+ # d['engine'] = None
2454
+ d["_log"] = None
2455
+ d["gdrive"] = None
2456
+ d["gauth"] = None
2457
+ d["net_lock"] = None
2458
+ return d
2459
+
2460
+ def __setstate__(self, d):
2461
+ """We reconfigure on opening a pickle"""
2462
+ self.debug("seralizing")
2463
+ for dkey, dval in d.items():
2464
+ if dkey in self.keep_keys:
2465
+ self.__dict__[dkey] = dval
2466
+
2467
+ # Force distributed drive contexts to be single threaded
2468
+ self._use_threadpool = False
2469
+ self._max_num_threads = 1
2470
+ self.time_fuzz = 6
2471
+
2472
+ self._hot_directories = ExpiringDict(max_len=100, max_age_seconds=1)
2473
+
2474
+ self.net_lock = threading.RLock()
2475
+ if self.gauth is None:
2476
+ self.authoirze_google_integrations()
2477
+
2478
+ for item in self.nodes:
2479
+ item._drive = self
2480
+ if isinstance(
2481
+ item._item, pydrive2.files.GoogleDriveFile
2482
+ ): # catch shared drives and skip
2483
+ item._item.__dict__["attr"]["auth"] = self.gauth
2484
+
2485
+ @property
2486
+ def fs_cache_filename(self):
2487
+ """a property to provide a default local filesystem name for caching"""
2488
+ return f".engforgedrive_fs_{ self.full_sync_root.replace('shared:','').replace(os.sep,'_') }.pk"
2489
+
2490
+ def save(self):
2491
+ try:
2492
+ self.info(f"saving {self.fs_cache_filename}!")
2493
+ with open(self.fs_cache_filename, "wb") as fp:
2494
+ fp.write(pickle.dumps(self))
2495
+ # BUG: Cannot pickle io.BufferedReader
2496
+
2497
+ except Exception as e:
2498
+ self.error(e, "Issue Saving File System")
2499
+
2500
+ def read(self):
2501
+ """:returns: success as bool"""
2502
+ try:
2503
+ self.info(f"reading {self.fs_cache_filename}!")
2504
+ with open(self.fs_cache_filename, "rb") as fp:
2505
+ old = pickle.loads(fp.read())
2506
+ # TODO: intellegently load old file system, should be mostly right but how to handle differences
2507
+ # 1) Perhaps compare file system to folders that need syncing and list the intersection
2508
+ # 2) for the items in the list audit the lowest levels up, creating directories as needed
2509
+ # 3) Maybe compare date of pickle to date of files and whats on the drive.
2510
+
2511
+ # Alternate Idea
2512
+ # 4) Thread Read Google Drive and Local FS in independent threads building linked graphs
2513
+
2514
+ except Exception as e:
2515
+ self.error(e, "Issue Reading File System")
2516
+ return False
2517
+ else:
2518
+ return True
2519
+
2520
+
2521
+ gpi = logging.getLogger("googleapiclient.http")
2522
+ gpi.setLevel(60)
2523
+ gpi = logging.getLogger("oauth2client.transport")
2524
+ gpi.setLevel(50)
2525
+ gpi = logging.getLogger("oauth2client.client")
2526
+ gpi.setLevel(50)
2527
+
2528
+
2529
+ def main_cli():
2530
+ import argparse
2531
+
2532
+ parser = argparse.ArgumentParser("Otter Drive Sync From Folder")
2533
+
2534
+ parser.add_argument("--shared-drive", "-D", default=None, help="shared drive name")
2535
+ parser.add_argument(
2536
+ "--syncpath",
2537
+ "-S",
2538
+ default=None,
2539
+ help="sync path relative to shared drive name",
2540
+ )
2541
+ parser.add_argument(
2542
+ "--filepath",
2543
+ "-F",
2544
+ default=None,
2545
+ help="filpath to sync to google shared_drive:syncpath, otherwise it will start here!",
2546
+ )
2547
+ parser.add_argument("--verbose", "-v", action="store_true", help="verbose")
2548
+ parser.add_argument(
2549
+ "--creds",
2550
+ "-c",
2551
+ default=None,
2552
+ help="path to service account credentials, can be a json file, or a directory where its stored. ",
2553
+ )
2554
+ parser.add_argument(
2555
+ "--dry-run",
2556
+ "-R",
2557
+ action="store_true",
2558
+ help="dry run, dont do any work! (WIP)",
2559
+ )
2560
+ parser.add_argument("--sync", action="store_true", help="sync the directories!")
2561
+ parser.add_argument(
2562
+ "--remove-duplicates", action="store_true", help="remove any duplicates"
2563
+ )
2564
+
2565
+ args = parser.parse_args()
2566
+
2567
+ if args.verbose:
2568
+ set_all_loggers_to(logging.DEBUG)
2569
+ else:
2570
+ gpi = logging.getLogger("googleapiclient.http")
2571
+ gpi.setLevel(60)
2572
+
2573
+ od = OtterDrive(
2574
+ shared_drive=args.shared_drive,
2575
+ sync_root=args.syncpath,
2576
+ filepath_root=args.filepath,
2577
+ creds_path=args.creds,
2578
+ dry_run=args.dry_run,
2579
+ )
2580
+
2581
+ if args.sync:
2582
+ od.sync()
2583
+ if args.remove_duplicates or all((args.sync, od.duplicates_exist)):
2584
+ od.remove_duplicates()
2585
+
2586
+ od.save()
2587
+
2588
+
2589
+ if __name__ == "__main__":
2590
+ main_cli()
2591
+
2592
+
2593
+ # def copy_file(self, origin_id, target_id = None, create_filename=None, parent_id = None):
2594
+ # self.debug(f'copy file: {origin_id}, {target_id}, {create_filename}, {parent_id} ')
2595
+
2596
+ # try:
2597
+ # assert not any((all( (target_id is None, create_filename is None)) , parent_id is None))
2598
+
2599
+ # origin_file = self.create_file({'id' : origin_id })
2600
+
2601
+ # if origin_file is not None and 'mimeType' in origin_file and origin_file['mimeType'] == 'text/plain':
2602
+ # #We can version plain text!
2603
+ # content = origin_file.GetContentString()
2604
+
2605
+ # if origin_file is not None and origin_file.http is not None:
2606
+ # for c in origin_file.http.connections.values():
2607
+ # c.close()
2608
+ # else:
2609
+ # return None
2610
+ # #TODO: Somehow Version Non Plain Text Files
2611
+ # #origin_file.FetchContent()
2612
+ # #content = origin_file.content
2613
+
2614
+ # self.sleep()
2615
+
2616
+ # if target_id is not None: #We know origin and target, good to go!
2617
+ # #We're just getting the reference!
2618
+ # target_file = self.upload_or_update_file( parent_id, file_id = target_id, content= content)
2619
+ # return target_file
2620
+
2621
+ # elif create_filename is not None:
2622
+ # target_file = self.upload_or_update_file( parent_id , file_path =create_filename , content= content )
2623
+ # return target_file
2624
+
2625
+ # except Exception as e:
2626
+ # self.error(e,f'Could not copy file {origin_id}, {target_id}, {create_filename}, {parent_id}')
2627
+
2628
+ # def merge_folder(self, target_id, origin_id):
2629
+ # '''NOT READY: Use to map origin to target
2630
+ # #FIXME: instead of actually moving the content, just add the appropriate parent id and then check duplicates again'''
2631
+
2632
+ # self.debug(f'merge_folder: {origin_id}, {target_id}')
2633
+
2634
+ # target_files = self.cache_directory(target_id) #This will take care of duplicates in target folder via search
2635
+ # other_files = self.cache_directory(origin_id) #This will take care of duplicates in other folder via search
2636
+ # target_titles = { tar['title']:tar['id'] for tar in target_files }
2637
+
2638
+ # for ofil in other_files:
2639
+ # otitle = ofil['title']
2640
+ # oid = ofil['id']
2641
+ # if self.is_folder(ofil):
2642
+ # fol_id = self.get_or_create_folder( otitle ,parent_id = target_id)
2643
+ # self.merge_folder( fol_id, oid)
2644
+
2645
+ # if self.is_file(ofil):
2646
+ # if otitle in target_titles: #update it
2647
+ # fil = self.copy_file( oid, target_id = target_titles[otitle], parent_id = target_id)
2648
+
2649
+ # else: #create it
2650
+ # fil = self.copy_file( oid, create_filename = otitle, parent_id = target_id)