New Addon
Google BK
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# flake8: noqa
|
||||
from .asynchttpgetter import AsyncHttpGetter
|
||||
from .backoff import Backoff
|
||||
from .estimator import Estimator
|
||||
from .globalinfo import GlobalInfo
|
||||
from .resolver import Resolver
|
||||
from .rangelookup import RangeLookup
|
||||
from .data_cache import DataCache, KEY_CREATED, KEY_I_MADE_THIS, KEY_PENDING, KEY_NOTE, KEY_IGNORE, KEY_LAST_SEEN, KEY_NAME, CACHE_EXPIRATION_DAYS, UpgradeFlags
|
||||
@@ -0,0 +1,241 @@
|
||||
from datetime import timedelta
|
||||
import io
|
||||
from typing import Dict
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client import ClientResponse, ClientPayloadError, ClientOSError
|
||||
from asyncio.exceptions import TimeoutError
|
||||
from collections import deque
|
||||
|
||||
from ..exceptions import LogicError, ensureKey
|
||||
from ..logger import getLogger
|
||||
from ..time import Time
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
CONTENT_LENGTH_HEADER = "content-length"
|
||||
CONTENT_LENGTH_ERROR = "Content size must be provided if the webserver doesn't provide it"
|
||||
SERVER_CONTENT_LENGTH_ERROR = "Server returned a content length that didn't match the requested size"
|
||||
POSITION_ERROR_MESSAGE = "AsyncHttpGetter must also be set up at position 0"
|
||||
DEFAULT_CHUNK_SIZE = 1024 * 1024
|
||||
|
||||
|
||||
# This class is dumb but it gets around a dumb problem
|
||||
class Stupid(io.BytesIO):
|
||||
def __len__(self):
|
||||
return len(self.getvalue())
|
||||
|
||||
|
||||
class AsyncHttpGetter:
|
||||
def __init__(self, url, headers: Dict[str, str], session, size: int = None, timeout=None, timeoutFactory=None, otherErrorFactory=None, time: Time = None):
|
||||
self._url: str = url
|
||||
|
||||
# Current position of the stream
|
||||
self._position: int = 0
|
||||
|
||||
# Total size of the stream
|
||||
self._size: int = size
|
||||
|
||||
# Headers that should get sent with every request
|
||||
self._headers: Dict[str, str] = headers
|
||||
|
||||
# Session used to make http requests.
|
||||
self._session: ClientSession = session
|
||||
|
||||
# Most recent session request
|
||||
self._response: ClientResponse = None
|
||||
|
||||
# Where the resposne currently starts
|
||||
self._responseStart = 0
|
||||
|
||||
self._history = deque()
|
||||
self._time = time
|
||||
self._startTime = self._time.now()
|
||||
self.timeoutFactory = timeoutFactory
|
||||
self.otherErrorFactory = otherErrorFactory
|
||||
self.timeout = timeout
|
||||
|
||||
async def setup(self):
|
||||
if not self._position == 0:
|
||||
raise LogicError(POSITION_ERROR_MESSAGE)
|
||||
await self._startReadRemoteAt(0)
|
||||
if CONTENT_LENGTH_HEADER in self._response.headers:
|
||||
self._size = int(ensureKey(
|
||||
CONTENT_LENGTH_HEADER, self._response.headers, "web server get request's headers"))
|
||||
elif self._size is None:
|
||||
raise LogicError(
|
||||
CONTENT_LENGTH_ERROR)
|
||||
|
||||
self._history.append([self._time.now(), 0])
|
||||
return self._size
|
||||
|
||||
def _ensureSetup(self):
|
||||
if self._size is None:
|
||||
raise LogicError("AsyncHttpGetter.setup() must be called first")
|
||||
|
||||
def size(self) -> int:
|
||||
self._ensureSetup()
|
||||
return self._size
|
||||
|
||||
def __len__(self):
|
||||
return self.size()
|
||||
|
||||
def position(self, pos=None):
|
||||
if pos is not None:
|
||||
self._position = pos
|
||||
return self._position
|
||||
|
||||
async def generator(self, chunk_size):
|
||||
while True:
|
||||
chunk = await self.read(chunk_size)
|
||||
if len(chunk.getbuffer()) == 0:
|
||||
break
|
||||
yield chunk.getbuffer()
|
||||
|
||||
def progress(self):
|
||||
if self._size == 0:
|
||||
return 0
|
||||
self._ensureSetup()
|
||||
if self._size == 0:
|
||||
return 0
|
||||
return 100 * float(self.position()) / float(self._size)
|
||||
|
||||
# return the estimated speed of the tranfser in bytes/second
|
||||
def speed(self, period: timedelta = timedelta(seconds=10)):
|
||||
if len(self._history) < 2:
|
||||
return None
|
||||
now = self._time.now()
|
||||
intervals = []
|
||||
current = self._history[0]
|
||||
last_speed = 0
|
||||
for x in range(1, len(self._history)):
|
||||
next = self._history[x]
|
||||
seconds = (next[0] - current[0]).total_seconds()
|
||||
data = next[1] - current[1]
|
||||
if seconds == 0:
|
||||
speed = 0 # avoid div by 0
|
||||
else:
|
||||
speed = data / seconds
|
||||
intervals.append([current[0], next[0], speed])
|
||||
current = next
|
||||
last_speed = speed
|
||||
|
||||
time_since_last = now - current[0]
|
||||
if time_since_last > period:
|
||||
# linear decay the "estimated" rate over the period
|
||||
diff = time_since_last - period
|
||||
if diff > period:
|
||||
return 0
|
||||
else:
|
||||
return last_speed * (1 - diff.total_seconds() / period.total_seconds())
|
||||
|
||||
# behave as though we continued with the pseed form the last interval
|
||||
intervals.append([current[0], now, last_speed])
|
||||
|
||||
# calculate the time-averaged speed over the given period
|
||||
stop = now
|
||||
start = now - period
|
||||
total = 0
|
||||
minimum_time = now
|
||||
for interval in intervals:
|
||||
if start > interval[1]:
|
||||
continue
|
||||
if stop < interval[0]:
|
||||
continue
|
||||
overlap_start = max(start, interval[0])
|
||||
overlap_stop = min(stop, interval[1])
|
||||
if overlap_stop > overlap_start:
|
||||
total += (overlap_stop - overlap_start).total_seconds() * interval[2]
|
||||
if overlap_start < minimum_time:
|
||||
minimum_time = overlap_start
|
||||
return total / (now - minimum_time).total_seconds()
|
||||
|
||||
def startTime(self):
|
||||
return self._startTime
|
||||
|
||||
def __format__(self, format_spec: str) -> str:
|
||||
return str(int(self.progress()))
|
||||
|
||||
async def _startReadRemoteAt(self, where: int):
|
||||
headers = self._headers.copy()
|
||||
# request a byte range
|
||||
if where != 0:
|
||||
headers['range'] = "bytes=%s-%s" % (self._position, self._size - 1)
|
||||
if self._response is not None:
|
||||
self._response.release()
|
||||
try:
|
||||
resp = await self._session.get(self._url, headers=headers, timeout=self.timeout)
|
||||
except TimeoutError:
|
||||
if self.timeoutFactory is not None:
|
||||
raise self.timeoutFactory()
|
||||
raise
|
||||
except ClientPayloadError:
|
||||
if self.otherErrorFactory is not None:
|
||||
raise self.otherErrorFactory()
|
||||
raise
|
||||
except ClientOSError:
|
||||
if self.otherErrorFactory is not None:
|
||||
raise self.otherErrorFactory()
|
||||
raise
|
||||
resp.raise_for_status()
|
||||
if where == 0 and self._size is not None and CONTENT_LENGTH_HEADER in resp.headers and int(resp.headers[CONTENT_LENGTH_HEADER]) != self._size:
|
||||
resp.release()
|
||||
raise LogicError(SERVER_CONTENT_LENGTH_ERROR)
|
||||
self._response = resp
|
||||
self._responseStart = where
|
||||
|
||||
async def read(self, count=DEFAULT_CHUNK_SIZE):
|
||||
self._ensureSetup()
|
||||
ret = Stupid()
|
||||
if self._size is not None and self._position >= self._size:
|
||||
return ret
|
||||
|
||||
# See if we need to move the stream elsewhere
|
||||
if self._responseStart != self._position:
|
||||
# Reset the stream's position
|
||||
await self._startReadRemoteAt(self._position)
|
||||
|
||||
# Limit by how much we can get from the stream
|
||||
needed = min(count, self.size() - self._position)
|
||||
|
||||
# And then get it
|
||||
try:
|
||||
data = await self._response.content.readexactly(needed)
|
||||
except TimeoutError:
|
||||
if self.timeoutFactory is not None:
|
||||
raise self.timeoutFactory()
|
||||
raise
|
||||
except ClientPayloadError:
|
||||
if self.otherErrorFactory is not None:
|
||||
raise self.otherErrorFactory()
|
||||
raise
|
||||
except ClientOSError:
|
||||
if self.otherErrorFactory is not None:
|
||||
raise self.otherErrorFactory()
|
||||
raise
|
||||
ret.write(data)
|
||||
# Keep track of where we are in the stream
|
||||
self._responseStart += len(data)
|
||||
self._position += len(data)
|
||||
self._history.append([self._time.now(), self._position])
|
||||
if len(self._history) > 50:
|
||||
self._history.popleft()
|
||||
|
||||
ret.seek(0)
|
||||
return ret
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.setup()
|
||||
|
||||
async def __aexit__(self, type, value, traceback):
|
||||
if self._response is not None:
|
||||
self._response.release()
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
val = await self.read()
|
||||
if len(val) == 0:
|
||||
raise StopAsyncIteration
|
||||
return val.getbuffer()
|
||||
@@ -0,0 +1,42 @@
|
||||
from ..logger import getLogger
|
||||
|
||||
MAX_FACTOR = 20 # avoid weirdness with python's arbitary integer precision
|
||||
MAX_WAIT = 60 * 60 # 1 hour
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class Backoff():
|
||||
def __init__(self, initial=None, base=2, exp=2, max=MAX_WAIT, attempts=None):
|
||||
self._attempts = attempts
|
||||
self._initial = initial
|
||||
self._start = base
|
||||
self._max = max
|
||||
self._exp = exp
|
||||
self._counter = 0
|
||||
|
||||
def reset(self):
|
||||
self._counter = 0
|
||||
|
||||
def peek(self):
|
||||
exp = self._counter - 1
|
||||
if self._counter == 1 and self._initial is not None:
|
||||
return self._initial
|
||||
elif self._initial is not None:
|
||||
exp -= 1
|
||||
|
||||
exp = min(exp, MAX_FACTOR)
|
||||
computed = self._start * pow(self._exp, exp)
|
||||
|
||||
if self._max:
|
||||
computed = min(self._max, computed)
|
||||
return computed
|
||||
|
||||
def backoff(self, error):
|
||||
if self._attempts and self._counter >= self._attempts:
|
||||
raise error
|
||||
self._counter += 1
|
||||
return self.peek()
|
||||
|
||||
def maxOut(self):
|
||||
self._counter = 100
|
||||
@@ -0,0 +1,149 @@
|
||||
from datetime import timedelta
|
||||
from enum import Enum, unique
|
||||
from backup.config import Config, Setting, VERSION, Version
|
||||
from backup.file import JsonFileSaver
|
||||
from backup.const import NECESSARY_OLD_BACKUP_PLURAL_NAME
|
||||
from injector import inject, singleton
|
||||
from ..time import Time
|
||||
from typing import Dict
|
||||
import json
|
||||
import os
|
||||
|
||||
KEY_I_MADE_THIS = "i_made_this"
|
||||
KEY_PENDING = "pending"
|
||||
KEY_CREATED = "created"
|
||||
KEY_IGNORE = "ignore"
|
||||
KEY_LAST_SEEN = "last_seen"
|
||||
KEY_NAME = "name"
|
||||
KEY_LAST_VERSION = "last_verison"
|
||||
KEY_UPGRADES = "upgrades"
|
||||
KEY_FLAGS = "flags"
|
||||
KEY_NOTE = "note"
|
||||
|
||||
CACHE_EXPIRATION_DAYS = 30
|
||||
|
||||
VERSION_DEFUALT_IGNORE_UPGRADES = Version.parse("0.108.2")
|
||||
|
||||
|
||||
@unique
|
||||
class UpgradeFlags(Enum):
|
||||
NOTIFIED_ABOUT_BACKUP_RENAME = "notified_backup_rename"
|
||||
NOTIFIED_ABOUT_IGNORED_BACKUPS = "notified_ignored_backups"
|
||||
NOTIFIED_ABOUT_OOB_FLOW = "notified_about_oob_flow"
|
||||
TESTING_FLAG = "testing_flag"
|
||||
|
||||
|
||||
@singleton
|
||||
class DataCache:
|
||||
@inject
|
||||
def __init__(self, config: Config, time: Time):
|
||||
self._config = config
|
||||
self._data = {}
|
||||
self._dirty = {}
|
||||
self._time = time
|
||||
self._last_version = Version.default()
|
||||
self._first_version = Version.default()
|
||||
self._flags = set()
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
path = self._config.get(Setting.DATA_CACHE_FILE_PATH)
|
||||
if not JsonFileSaver.exists(path):
|
||||
self._data = {NECESSARY_OLD_BACKUP_PLURAL_NAME: {}}
|
||||
else:
|
||||
self._data = JsonFileSaver.read(path)
|
||||
|
||||
# Check for an upgrade.
|
||||
if KEY_LAST_VERSION in self._data:
|
||||
self._last_version = Version.parse(self._data[KEY_LAST_VERSION])
|
||||
if self.previousVersion != self.currentVersion:
|
||||
# add an upgrade marker
|
||||
if KEY_UPGRADES not in self._data:
|
||||
self._data[KEY_UPGRADES] = []
|
||||
self._data[KEY_UPGRADES].append({
|
||||
'prev_version': str(self.previousVersion),
|
||||
'new_version': str(self.currentVersion),
|
||||
'date': self._time.now().isoformat()
|
||||
})
|
||||
self._data[KEY_LAST_VERSION] = str(self.currentVersion)
|
||||
self.makeDirty()
|
||||
if KEY_UPGRADES not in self._data or len(self._data[KEY_UPGRADES]) == 0:
|
||||
self._first_version = self.currentVersion
|
||||
else:
|
||||
self._first_version = Version.parse(self._data[KEY_UPGRADES][0]['new_version'])
|
||||
|
||||
if self._config.isExplicit(Setting.IGNORE_OTHER_BACKUPS) or self._config.isExplicit(Setting.IGNORE_UPGRADE_BACKUPS):
|
||||
self.addFlag(UpgradeFlags.NOTIFIED_ABOUT_IGNORED_BACKUPS)
|
||||
|
||||
if self.notifyForIgnoreUpgrades:
|
||||
self._config.useLegacyIgnoredBehavior(True)
|
||||
|
||||
self.saveIfDirty()
|
||||
|
||||
def save(self, data=None):
|
||||
if data is None:
|
||||
data = self._data
|
||||
path = self._config.get(Setting.DATA_CACHE_FILE_PATH)
|
||||
JsonFileSaver.write(path, data)
|
||||
self._dirty = False
|
||||
|
||||
def makeDirty(self):
|
||||
self._dirty = True
|
||||
|
||||
@property
|
||||
def dirty(self) -> bool:
|
||||
return self._dirty
|
||||
|
||||
@property
|
||||
def backups(self) -> Dict[str, Dict[str, str]]:
|
||||
if NECESSARY_OLD_BACKUP_PLURAL_NAME not in self._data:
|
||||
self._data[NECESSARY_OLD_BACKUP_PLURAL_NAME] = {}
|
||||
return self._data[NECESSARY_OLD_BACKUP_PLURAL_NAME]
|
||||
|
||||
def backup(self, slug) -> Dict[str, str]:
|
||||
if slug not in self.backups:
|
||||
self.backups[slug] = {}
|
||||
return self.backups[slug]
|
||||
|
||||
def saveIfDirty(self):
|
||||
if self._dirty:
|
||||
# See if we need to remove any old entries
|
||||
for slug in list(self.backups.keys()):
|
||||
data = self.backups[slug].get(KEY_LAST_SEEN)
|
||||
if data is not None and self._time.now() > self._time.parse(data) + timedelta(days=CACHE_EXPIRATION_DAYS):
|
||||
del self.backups[slug]
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def previousVersion(self):
|
||||
return self._last_version
|
||||
|
||||
@property
|
||||
def firstVersion(self):
|
||||
return self._first_version
|
||||
|
||||
@property
|
||||
def currentVersion(self):
|
||||
return Version.parse(VERSION)
|
||||
|
||||
@property
|
||||
def notifyForIgnoreUpgrades(self):
|
||||
return self.firstVersion < VERSION_DEFUALT_IGNORE_UPGRADES and not self.checkFlag(UpgradeFlags.NOTIFIED_ABOUT_IGNORED_BACKUPS) and not self._config.isExplicit(Setting.IGNORE_OTHER_BACKUPS) and not self._config.isExplicit(Setting.IGNORE_UPGRADE_BACKUPS)
|
||||
|
||||
def checkFlag(self, flag: UpgradeFlags):
|
||||
return flag.value in self._data.get(KEY_FLAGS, [])
|
||||
|
||||
def TESTS_ONLY_clearFlags(self):
|
||||
self._data[KEY_FLAGS] = []
|
||||
|
||||
def addFlag(self, flag: UpgradeFlags):
|
||||
all_flags = set(self._data.get(KEY_FLAGS, []))
|
||||
all_flags.add(flag.value)
|
||||
self._data[KEY_FLAGS] = list(all_flags)
|
||||
self.makeDirty()
|
||||
|
||||
def getUpgradeTime(self, version: Version):
|
||||
for upgrade in self._data[KEY_UPGRADES]:
|
||||
if Version.parse(upgrade['new_version']) >= version:
|
||||
return self._time.parse(upgrade['date'])
|
||||
return self._time.now()
|
||||
@@ -0,0 +1,97 @@
|
||||
import asyncio
|
||||
import firebase_admin
|
||||
from firebase_admin import credentials
|
||||
from firebase_admin import firestore
|
||||
from pathlib import Path
|
||||
|
||||
KNWON_ERRORS = ['existing_backup_folder', 'google_dns', 'cancelled', 'google_timeout', 'low_space', 'multiple_deletes']
|
||||
|
||||
|
||||
async def main():
|
||||
cred = credentials.Certificate(str(Path.home().joinpath("Documents/secrets/server-firestore-creds.json")))
|
||||
firebase_admin.initialize_app(cred)
|
||||
|
||||
db = firestore.client()
|
||||
reports = db.collection(u'error_reports')
|
||||
|
||||
while(True):
|
||||
command = input()
|
||||
if command == "clear all":
|
||||
print("Deleteing records...")
|
||||
batch = db.batch()
|
||||
batch_size = 0
|
||||
for item in reports.stream():
|
||||
batch.delete(reports.document(item.id))
|
||||
batch_size += 1
|
||||
batch.commit()
|
||||
print("Done")
|
||||
if command == "":
|
||||
# Do regular analysis
|
||||
await stats(db, reports)
|
||||
if command.startswith("get "):
|
||||
query = reports.where("report.client", "==", command[4:])
|
||||
for report in query.get():
|
||||
print(report.to_dict())
|
||||
print("done")
|
||||
elif command == "exit" or command == "quit" or command == "q":
|
||||
return
|
||||
|
||||
# stream = reports.stream()
|
||||
for report in query.get():
|
||||
print(report.to_dict())
|
||||
|
||||
|
||||
async def stats(db, reports):
|
||||
print("Inspecting the last 24 hours of records...")
|
||||
cursor = None
|
||||
batch_size = 50
|
||||
summary = {}
|
||||
resolved = {}
|
||||
total = 0
|
||||
while True:
|
||||
count = 0
|
||||
if cursor is None:
|
||||
stream = reports.limit(batch_size).stream()
|
||||
else:
|
||||
stream = reports.limit(batch_size).start_after(cursor).stream()
|
||||
for report in stream:
|
||||
cursor = report
|
||||
data = report.to_dict()
|
||||
count += 1
|
||||
total += 1
|
||||
if 'report' not in data:
|
||||
continue
|
||||
if 'error' not in data['report']:
|
||||
resolved[data['client']] = data['report']['duration']
|
||||
continue
|
||||
if data['report']['error'] in summary:
|
||||
summary[data['report']['error']].append(data)
|
||||
else:
|
||||
summary[data['report']['error']] = [data]
|
||||
print("Processed " + str(count) + " records")
|
||||
if count == 0:
|
||||
break
|
||||
for error in summary.keys():
|
||||
print("")
|
||||
distinct = set()
|
||||
unresolved = {}
|
||||
versions = set()
|
||||
for report in summary[error]:
|
||||
distinct.add(report['client'])
|
||||
if report['client'] not in resolved:
|
||||
unresolved[report['client']] = report
|
||||
versions.add(report['version'])
|
||||
print("Unresolved: " + str(len(unresolved)))
|
||||
print("Count: " + str(len(summary[error])))
|
||||
print("Distinct: " + str(len(distinct)))
|
||||
print("Versions: " + str(versions))
|
||||
print("Error: " + error)
|
||||
if len(unresolved) > 0:
|
||||
print("Unresolved: ")
|
||||
for unres in unresolved.values():
|
||||
print(" " + unres['report']['arch'] + " " + unres['client'])
|
||||
print("Total records: " + str(total))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,105 @@
|
||||
import os
|
||||
import platform
|
||||
|
||||
from injector import inject, singleton
|
||||
|
||||
from ..config import Config, Setting
|
||||
from ..exceptions import LowSpaceError
|
||||
from .globalinfo import GlobalInfo
|
||||
from ..logger import getLogger
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
SIZE_SI = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
||||
|
||||
|
||||
@singleton
|
||||
class Estimator():
|
||||
@inject
|
||||
def __init__(self, config: Config, global_info: GlobalInfo):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self._blocksUsed = 0
|
||||
self._blocksTotal = 1
|
||||
self._blockSize = 0
|
||||
self._global_info = global_info
|
||||
|
||||
def refresh(self):
|
||||
if platform.system() == "Windows":
|
||||
# Unsupported on windows
|
||||
return self
|
||||
# This roughly matches the results you get by running "df",
|
||||
# except its a bit more conservative, which isn't necessarily
|
||||
# correct, but we're aiming for ballpark numbers here so it
|
||||
# should be ok.
|
||||
stats = os.statvfs(self.config.get(Setting.BACKUP_DIRECTORY_PATH))
|
||||
total = stats.f_blocks
|
||||
available = stats.f_bavail
|
||||
availableToRoot = stats.f_bfree
|
||||
self._blocksUsed = total - availableToRoot
|
||||
self._blocksTotal = self._blocksUsed + available
|
||||
self._blockSize = stats.f_frsize
|
||||
return self
|
||||
|
||||
def checkSpace(self, backups):
|
||||
if platform.system() == "Windows":
|
||||
# Unsupported on windows
|
||||
return
|
||||
if not self.config.get(Setting.WARN_FOR_LOW_SPACE):
|
||||
# Don't check, just go
|
||||
return
|
||||
try:
|
||||
self._checkSpace(backups)
|
||||
except Exception as e:
|
||||
if isinstance(e, LowSpaceError):
|
||||
raise e
|
||||
# Just log the error and continue otherwise
|
||||
logger.error(
|
||||
"Encountered an error while trying to check disk space remaining: " + str(e))
|
||||
|
||||
def _checkSpace(self, backups):
|
||||
# get the most recent backup size
|
||||
space_needed = self.config.get(Setting.LOW_SPACE_THRESHOLD)
|
||||
backups.sort(key=lambda s: s.date(), reverse=True)
|
||||
for backup in backups:
|
||||
latest_size = backup.sizeInt()
|
||||
if latest_size > 1:
|
||||
# Bump the size a little to estimate organic growth.
|
||||
space_needed = latest_size * 1.1
|
||||
break
|
||||
|
||||
if space_needed > self.getBytesFree() and not self._global_info.isSkipSpaceCheckOnce():
|
||||
raise LowSpaceError("{0}%".format(
|
||||
int(self.getUsagePercent())), Estimator.asSizeString(self.getBytesFree()))
|
||||
|
||||
def getUsagePercent(self):
|
||||
if self.getBlocksTotal() == 0:
|
||||
return 0
|
||||
return 100.0 * float(self.getBlocksUsed()) / float(self.getBlocksTotal())
|
||||
|
||||
def getBlocksUsed(self):
|
||||
return self._blocksUsed
|
||||
|
||||
def getBlocksTotal(self):
|
||||
return self._blocksTotal
|
||||
|
||||
def getBlocksFree(self):
|
||||
return self.getBlocksTotal() - self.getBlocksUsed()
|
||||
|
||||
def getBytesFree(self):
|
||||
return self._blockSize * self.getBlocksFree()
|
||||
|
||||
def getBytesUsed(self):
|
||||
return self._blockSize * self.getBlocksUsed()
|
||||
|
||||
def getBytesTotal(self):
|
||||
return self._blockSize * self.getBlocksTotal()
|
||||
|
||||
@classmethod
|
||||
def asSizeString(cls, size):
|
||||
current = float(size)
|
||||
for id in SIZE_SI:
|
||||
if current < 1024:
|
||||
return "{0} {1}".format(round(current, 1), id)
|
||||
current /= 1024
|
||||
return "Beyond mortal comprehension"
|
||||
@@ -0,0 +1,125 @@
|
||||
|
||||
from datetime import timedelta
|
||||
from threading import Lock
|
||||
|
||||
from injector import inject, singleton
|
||||
|
||||
from ..time import Time
|
||||
from ..logger import getLogger
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
@singleton
|
||||
class GlobalInfo():
|
||||
@inject
|
||||
def __init__(self, time: Time):
|
||||
self._time = time
|
||||
self._syncs = 0
|
||||
self._failures = 0
|
||||
self._last_error = 0
|
||||
self._successes = 0
|
||||
self._last_failure_time = None
|
||||
self._uploads = 0
|
||||
self._last_upload = None
|
||||
self._last_success = time.now()
|
||||
self._last_sync_success = None
|
||||
self._last_upload_size = None
|
||||
self._last_sync_start = None
|
||||
self._last_error = None
|
||||
self._supress_error = False
|
||||
self.credVersion = 0
|
||||
self._first_sync = True
|
||||
self._multipleDeletesPermitted = False
|
||||
self._dns_info = None
|
||||
self._skip_space_check_once = False
|
||||
self._ignore_errors_for_now = False
|
||||
self._start_time = time.now()
|
||||
|
||||
self.ha_ssl = False
|
||||
self.addons = None
|
||||
self.ha_port = None
|
||||
self.slug = None
|
||||
self.url = ""
|
||||
self.debug = {}
|
||||
self.lock = Lock()
|
||||
self.backup_cooldown_time = time.now()
|
||||
|
||||
def ignoreErrorsForNow(self):
|
||||
return self._ignore_errors_for_now
|
||||
|
||||
def setIngoreErrorsForNow(self, value):
|
||||
self._ignore_errors_for_now = value
|
||||
|
||||
def failureCount(self):
|
||||
return self._failures
|
||||
|
||||
def refresh(self):
|
||||
pass
|
||||
|
||||
def setDnsInfo(self, info):
|
||||
self._dns_info = info
|
||||
|
||||
@property
|
||||
def start_time(self):
|
||||
return self._start_time
|
||||
|
||||
def getDnsInfo(self):
|
||||
return self._dns_info
|
||||
|
||||
def success(self):
|
||||
self._first_sync = False
|
||||
self._last_error = None
|
||||
self._last_success = self._time.now()
|
||||
self._last_sync_success = self._time.now()
|
||||
self._successes += 1
|
||||
self._multipleDeletesPermitted = False
|
||||
self.setIngoreErrorsForNow(False)
|
||||
|
||||
def sync(self):
|
||||
self._last_sync_start = self._time.now()
|
||||
self._syncs += 1
|
||||
|
||||
def failed(self, error):
|
||||
self._first_sync = False
|
||||
self._last_error = error
|
||||
self._failures += 1
|
||||
self._last_failure_time = self._time.now()
|
||||
self._supress_error = False
|
||||
self.setIngoreErrorsForNow(False)
|
||||
|
||||
def suppressError(self):
|
||||
self._supress_error = True
|
||||
|
||||
def isErrorSuppressed(self):
|
||||
return self.suppressError
|
||||
|
||||
def upload(self, size):
|
||||
self._last_upload = self._time.now()
|
||||
self._uploads += 1
|
||||
self._last_upload_size = size
|
||||
|
||||
def credsSaved(self):
|
||||
self.credVersion += 1
|
||||
|
||||
def isPermitMultipleDeletes(self) -> bool:
|
||||
return self._multipleDeletesPermitted
|
||||
|
||||
def allowMultipleDeletes(self) -> bool:
|
||||
self._multipleDeletesPermitted = True
|
||||
|
||||
def addDebugInfo(self, key, value):
|
||||
with self.lock:
|
||||
self.debug[key] = value
|
||||
|
||||
def isSkipSpaceCheckOnce(self):
|
||||
return self._skip_space_check_once
|
||||
|
||||
def setSkipSpaceCheckOnce(self, val):
|
||||
self._skip_space_check_once = val
|
||||
|
||||
def triggerBackupCooldown(self, delay: timedelta):
|
||||
self.backup_cooldown_time = self._time.now() + delay
|
||||
|
||||
def backupCooldownTime(self):
|
||||
return self.backup_cooldown_time
|
||||
@@ -0,0 +1,56 @@
|
||||
from typing import List, TypeVar, Generic, Mapping
|
||||
|
||||
T = TypeVar('T')
|
||||
K = TypeVar('K')
|
||||
|
||||
|
||||
class RangeLookup(Generic[T, K]):
|
||||
def __init__(self, source: List[T], mapping: Mapping[T, K]):
|
||||
self.source = source
|
||||
self.map = mapping
|
||||
|
||||
def matches(self, start: K, end: K):
|
||||
range_start = self._searchFirstGreaterOrEqual(start)
|
||||
range_end = self._searchLastLessOrEqual(end)
|
||||
|
||||
x = range_start
|
||||
while (x <= range_end):
|
||||
if x >= 0 and x < len(self.source):
|
||||
yield self.source[x]
|
||||
x += 1
|
||||
|
||||
def matchList(self, start, end):
|
||||
ret = []
|
||||
for x in self.matches(start, end):
|
||||
ret.append(x)
|
||||
return ret
|
||||
|
||||
def _searchFirstGreaterOrEqual(self, val):
|
||||
first = 0
|
||||
last = len(self.source) - 1
|
||||
if self.map(self.source[last]) < val:
|
||||
return last + 1
|
||||
while first != last:
|
||||
mid = int((first + last) / 2)
|
||||
if self.map(self.source[mid]) < val:
|
||||
first = mid + 1
|
||||
else:
|
||||
last = mid
|
||||
return first
|
||||
|
||||
def _searchLastLessOrEqual(self, val):
|
||||
first = 0
|
||||
last = len(self.source) - 1
|
||||
if self.map(self.source[0]) > val:
|
||||
return -1
|
||||
while first != last:
|
||||
mid = int((first + last) / 2)
|
||||
if self.map(self.source[mid]) > val:
|
||||
last = mid - 1
|
||||
elif first != mid:
|
||||
first = mid
|
||||
elif self.map(self.source[last]) <= val:
|
||||
return last
|
||||
else:
|
||||
return first
|
||||
return first
|
||||
@@ -0,0 +1,58 @@
|
||||
import socket
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import aiodns
|
||||
from aiohttp.resolver import AsyncResolver
|
||||
from injector import inject, singleton
|
||||
|
||||
from ..config import Config, Setting
|
||||
from ..logger import getLogger
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
TTL_HOURS = 12
|
||||
|
||||
|
||||
@singleton
|
||||
class Resolver(AsyncResolver):
|
||||
@inject
|
||||
def __init__(self, config: Config):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self._original_dns = self._resolver
|
||||
self.setAlternateResolver()
|
||||
config.subscribe(self.updateConfig)
|
||||
|
||||
async def resolve(self, host: str, port: int = 0,
|
||||
family: int = socket.AF_INET) -> List[Dict[str, Any]]:
|
||||
if host == self.config.get(Setting.DRIVE_HOST_NAME) and len(self.config.get(Setting.DRIVE_IPV4)) > 0 and family == 0:
|
||||
# return the "mocked" drive address instead.
|
||||
return [{
|
||||
'family': 0,
|
||||
'flags': socket.AddressInfo.AI_NUMERICHOST,
|
||||
'port': port,
|
||||
'proto': 0,
|
||||
'host': self.config.get(Setting.DRIVE_IPV4),
|
||||
'hostname': host
|
||||
}]
|
||||
addresses = await super().resolve(host, port, family)
|
||||
return addresses
|
||||
|
||||
def updateConfig(self):
|
||||
if self._alt_ns != self.config.get(Setting.ALTERNATE_DNS_SERVERS):
|
||||
self.setAlternateResolver()
|
||||
self._resolver = self._alt_dns
|
||||
|
||||
def setAlternateResolver(self):
|
||||
if len(self.config.get(Setting.ALTERNATE_DNS_SERVERS)) > 0:
|
||||
self._alt_dns = aiodns.DNSResolver(loop=self._loop, nameservers=self.config.get(
|
||||
Setting.ALTERNATE_DNS_SERVERS).split(","))
|
||||
else:
|
||||
self._alt_dns = self._original_dns
|
||||
self._alt_ns = self.config.get(Setting.ALTERNATE_DNS_SERVERS)
|
||||
|
||||
def toggle(self):
|
||||
if self._resolver == self._original_dns:
|
||||
self._resolver = self._alt_dns
|
||||
else:
|
||||
self._resolver = self._original_dns
|
||||
Reference in New Issue
Block a user