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.
- engforge/__init__.py +26 -0
- engforge/_testing_components.py +864 -0
- engforge/analysis.py +257 -0
- engforge/attr_dynamics.py +222 -0
- engforge/attr_plotting.py +811 -0
- engforge/attr_signals.py +116 -0
- engforge/attr_slots.py +211 -0
- engforge/attr_solver.py +443 -0
- engforge/attributes.py +326 -0
- engforge/common.py +76 -0
- engforge/component_collections.py +276 -0
- engforge/components.py +55 -0
- engforge/configuration.py +673 -0
- engforge/dataframe.py +185 -0
- engforge/datastores/__init__.py +116 -0
- engforge/datastores/data.py +519 -0
- engforge/datastores/gdocs.py +2650 -0
- engforge/datastores/reporting.py +935 -0
- engforge/datastores/secrets.py +549 -0
- engforge/dynamics.py +662 -0
- engforge/eng/__init__.py +0 -0
- engforge/eng/costs.py +1459 -0
- engforge/eng/fluid_material.py +279 -0
- engforge/eng/geometry.py +1060 -0
- engforge/eng/pipes.py +1211 -0
- engforge/eng/prediction.py +366 -0
- engforge/eng/solid_materials.py +445 -0
- engforge/eng/structure.py +2228 -0
- engforge/eng/structure_beams.py +821 -0
- engforge/eng/thermodynamics.py +271 -0
- engforge/engforge_attributes.py +380 -0
- engforge/env_var.py +214 -0
- engforge/locations.py +18 -0
- engforge/logging.py +290 -0
- engforge/patterns.py +449 -0
- engforge/problem_context.py +2251 -0
- engforge/properties.py +501 -0
- engforge/reporting.py +595 -0
- engforge/solveable.py +964 -0
- engforge/solver.py +316 -0
- engforge/solver_utils.py +726 -0
- engforge/system.py +143 -0
- engforge/system_reference.py +299 -0
- engforge/tabulation.py +288 -0
- engforge/test/__init__.py +2 -0
- engforge/test/_pre_test_structures.py +221 -0
- engforge/test/report_testing.py +158 -0
- engforge/test/test_airfilter.py +91 -0
- engforge/test/test_analysis.py +3 -0
- engforge/test/test_comp_iter.py +196 -0
- engforge/test/test_composition.py +139 -0
- engforge/test/test_costs.py +338 -0
- engforge/test/test_dynamics.py +109 -0
- engforge/test/test_dynamics_spaces.py +39 -0
- engforge/test/test_four_bar.py +97 -0
- engforge/test/test_modules.py +72 -0
- engforge/test/test_performance.py +93 -0
- engforge/test/test_pipes.py +49 -0
- engforge/test/test_problem.py +411 -0
- engforge/test/test_problem_deepscoping.py +68 -0
- engforge/test/test_slider_crank.py +51 -0
- engforge/test/test_solver.py +343 -0
- engforge/test/test_tabulation.py +233 -0
- engforge/typing.py +92 -0
- engforge-0.3.0.dist-info/METADATA +392 -0
- engforge-0.3.0.dist-info/RECORD +69 -0
- engforge-0.3.0.dist-info/WHEEL +5 -0
- engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
- 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)
|