calibre-web/cps/gdriveutils.py
OzzieIsaacs 76d8cbdf57 Logging of details for gdrive integration only on debug level
fix generic cover path
browser reading for cbx and epub files no longer creates temp files on server
browser reading for cbx and epub files working (#502, #520)
2018-07-07 15:48:50 +02:00

359 lines
13 KiB
Python

try:
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from apiclient import errors
except ImportError:
pass
import os
from ub import config
import cli
from sqlalchemy import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import *
import web
engine = create_engine('sqlite:///{0}'.format(cli.gdpath), echo=False)
Base = declarative_base()
# Open session for database connection
Session = sessionmaker()
Session.configure(bind=engine)
session = scoped_session(Session)
class GdriveId(Base):
__tablename__ = 'gdrive_ids'
id = Column(Integer, primary_key=True)
gdrive_id = Column(Integer, unique=True)
path = Column(String)
__table_args__ = (UniqueConstraint('gdrive_id', 'path', name='_gdrive_path_uc'),)
def __repr__(self):
return str(self.path)
class PermissionAdded(Base):
__tablename__ = 'permissions_added'
id = Column(Integer, primary_key=True)
gdrive_id = Column(Integer, unique=True)
def __repr__(self):
return str(self.gdrive_id)
def migrate():
if not engine.dialect.has_table(engine.connect(), "permissions_added"):
PermissionAdded.__table__.create(bind = engine)
for sql in session.execute("select sql from sqlite_master where type='table'"):
if 'CREATE TABLE gdrive_ids' in sql[0]:
currUniqueConstraint = 'UNIQUE (gdrive_id)'
if currUniqueConstraint in sql[0]:
sql=sql[0].replace(currUniqueConstraint, 'UNIQUE (gdrive_id, path)')
sql=sql.replace(GdriveId.__tablename__, GdriveId.__tablename__ + '2')
session.execute(sql)
session.execute('INSERT INTO gdrive_ids2 (id, gdrive_id, path) SELECT id, gdrive_id, path FROM gdrive_ids;')
session.commit()
session.execute('DROP TABLE %s' % 'gdrive_ids')
session.execute('ALTER TABLE gdrive_ids2 RENAME to gdrive_ids')
break
if not os.path.exists(cli.gdpath):
try:
Base.metadata.create_all(engine)
except Exception:
raise
migrate()
def getDrive(drive=None, gauth=None):
if not drive:
if not gauth:
gauth = GoogleAuth(settings_file=os.path.join(config.get_main_dir,'settings.yaml'))
# Try to load saved client credentials
gauth.LoadCredentialsFile(os.path.join(config.get_main_dir,'gdrive_credentials'))
if gauth.access_token_expired:
# Refresh them if expired
try:
gauth.Refresh()
except:
web.app.logger.error("GDrive gdrive_credentials file not present, reauthenticate in config section")
else:
# Initialize the saved creds
gauth.Authorize()
# Save the current credentials to a file
return GoogleDrive(gauth)
if drive.auth.access_token_expired:
drive.auth.Refresh()
return drive
def listRootFolders(drive=None):
drive = getDrive(drive)
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
fileList = drive.ListFile({'q': folder}).GetList()
return fileList
def getEbooksFolder(drive=None):
return getFolderInFolder('root',config.config_google_drive_folder,drive)
def getFolderInFolder(parentId, folderName,drive=None):
drive = getDrive(drive)
query=""
if folderName:
query = "title = '%s' and " % folderName.replace("'", "\\'")
folder = query + "'%s' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false" % parentId
fileList = drive.ListFile({'q': folder}).GetList()
return fileList[0]
def getEbooksFolderId(drive=None):
storedPathName = session.query(GdriveId).filter(GdriveId.path == '/').first()
if storedPathName:
return storedPathName.gdrive_id
else:
gDriveId = GdriveId()
try:
gDriveId.gdrive_id = getEbooksFolder(drive)['id']
except:
pass
# ToDo Path not exisiting
gDriveId.path = '/'
session.merge(gDriveId)
session.commit()
return
def getFile(pathId, fileName, drive=None):
drive = getDrive(drive)
metaDataFile = "'%s' in parents and trashed = false and title = '%s'" % (pathId, fileName.replace("'", "\\'"))
fileList = drive.ListFile({'q': metaDataFile}).GetList()
return fileList[0]
def getFolderId(path, drive=None):
drive = getDrive(drive)
currentFolderId = getEbooksFolderId(drive)
sqlCheckPath = path if path[-1] == '/' else path + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
if not storedPathName:
dbChange = False
s = path.split('/')
for i, x in enumerate(s):
if len(x) > 0:
currentPath = "/".join(s[:i+1])
if currentPath[-1] != '/':
currentPath = currentPath + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == currentPath).first()
if storedPathName:
currentFolderId = storedPathName.gdrive_id
else:
currentFolderId = getFolderInFolder(currentFolderId, x, drive)['id']
gDriveId = GdriveId()
gDriveId.gdrive_id = currentFolderId
gDriveId.path = currentPath
session.merge(gDriveId)
dbChange = True
if dbChange:
session.commit()
else:
currentFolderId = storedPathName.gdrive_id
return currentFolderId
def getFileFromEbooksFolder(drive, path, fileName):
drive = getDrive(drive)
if path:
# sqlCheckPath=path if path[-1] =='/' else path + '/'
folderId = getFolderId(path, drive)
else:
folderId = getEbooksFolderId(drive)
return getFile(folderId, fileName, drive)
def copyDriveFileRemote(drive, origin_file_id, copy_title):
drive = getDrive(drive)
copied_file = {'title': copy_title}
try:
file_data = drive.auth.service.files().copy(
fileId = origin_file_id, body=copied_file).execute()
return drive.CreateFile({'id': file_data['id']})
except errors.HttpError as error:
print ('An error occurred: %s' % error)
return None
def downloadFile(drive, path, filename, output):
drive = getDrive(drive)
f = getFileFromEbooksFolder(drive, path, filename)
f.GetContentFile(output)
def backupCalibreDbAndOptionalDownload(drive, f=None):
drive = getDrive(drive)
metaDataFile = "'%s' in parents and title = 'metadata.db' and trashed = false" % getEbooksFolderId()
fileList = drive.ListFile({'q': metaDataFile}).GetList()
databaseFile = fileList[0]
if f:
databaseFile.GetContentFile(f)
def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
ignoreFiles=None,
parent=None, prevDir=''):
ignoreFiles = ignoreFiles or []
drive = getDrive(drive)
isInitial = not bool(parent)
if not parent:
parent = getEbooksFolder(drive)
if os.path.isdir(os.path.join(prevDir,uploadFile)):
existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % (os.path.basename(uploadFile), parent['id'])}).GetList()
if len(existingFolder) == 0 and (not isInitial or createRoot):
parent = drive.CreateFile({'title': os.path.basename(uploadFile), 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],
"mimeType": "application/vnd.google-apps.folder"})
parent.Upload()
else:
if (not isInitial or createRoot) and len(existingFolder) > 0:
parent = existingFolder[0]
for f in os.listdir(os.path.join(prevDir, uploadFile)):
if f not in ignoreFiles:
copyToDrive(drive, f, True, replaceFiles, ignoreFiles, parent, os.path.join(prevDir, uploadFile))
else:
if os.path.basename(uploadFile) not in ignoreFiles:
existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % (os.path.basename(uploadFile), parent['id'])}).GetList()
if len(existingFiles) > 0:
driveFile = existingFiles[0]
else:
driveFile = drive.CreateFile({'title': os.path.basename(uploadFile), 'parents': [{"kind":"drive#fileLink", 'id': parent['id']}], })
driveFile.SetContentFile(os.path.join(prevDir, uploadFile))
driveFile.Upload()
def uploadFileToEbooksFolder(drive, destFile, f):
drive = getDrive(drive)
parent = getEbooksFolder(drive)
splitDir = destFile.split('/')
for i, x in enumerate(splitDir):
if i == len(splitDir)-1:
existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % (x, parent['id'])}).GetList()
if len(existingFiles) > 0:
driveFile = existingFiles[0]
else:
driveFile = drive.CreateFile({'title': x, 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],})
driveFile.SetContentFile(f)
driveFile.Upload()
else:
existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % (x, parent['id'])}).GetList()
if len(existingFolder) == 0:
parent = drive.CreateFile({'title': x, 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],
"mimeType": "application/vnd.google-apps.folder"})
parent.Upload()
else:
parent = existingFolder[0]
def watchChange(drive, channel_id, channel_type, channel_address,
channel_token=None, expiration=None):
drive = getDrive(drive)
# Watch for all changes to a user's Drive.
# Args:
# service: Drive API service instance.
# channel_id: Unique string that identifies this channel.
# channel_type: Type of delivery mechanism used for this channel.
# channel_address: Address where notifications are delivered.
# channel_token: An arbitrary string delivered to the target address with
# each notification delivered over this channel. Optional.
# channel_address: Address where notifications are delivered. Optional.
# Returns:
# The created channel if successful
# Raises:
# apiclient.errors.HttpError: if http request to create channel fails.
body = {
'id': channel_id,
'type': channel_type,
'address': channel_address
}
if channel_token:
body['token'] = channel_token
if expiration:
body['expiration'] = expiration
return drive.auth.service.changes().watch(body=body).execute()
def watchFile(drive, file_id, channel_id, channel_type, channel_address,
channel_token=None, expiration=None):
"""Watch for any changes to a specific file.
Args:
service: Drive API service instance.
file_id: ID of the file to watch.
channel_id: Unique string that identifies this channel.
channel_type: Type of delivery mechanism used for this channel.
channel_address: Address where notifications are delivered.
channel_token: An arbitrary string delivered to the target address with
each notification delivered over this channel. Optional.
channel_address: Address where notifications are delivered. Optional.
Returns:
The created channel if successful
Raises:
apiclient.errors.HttpError: if http request to create channel fails.
"""
drive = getDrive(drive)
body = {
'id': channel_id,
'type': channel_type,
'address': channel_address
}
if channel_token:
body['token'] = channel_token
if expiration:
body['expiration'] = expiration
return drive.auth.service.files().watch(fileId=file_id, body=body).execute()
def stopChannel(drive, channel_id, resource_id):
"""Stop watching to a specific channel.
Args:
service: Drive API service instance.
channel_id: ID of the channel to stop.
resource_id: Resource ID of the channel to stop.
Raises:
apiclient.errors.HttpError: if http request to create channel fails.
"""
drive = getDrive(drive)
# service=drive.auth.service
body = {
'id': channel_id,
'resourceId': resource_id
}
return drive.auth.service.channels().stop(body=body).execute()
def getChangeById (drive, change_id):
drive = getDrive(drive)
# Print a single Change resource information.
#
# Args:
# service: Drive API service instance.
# change_id: ID of the Change resource to retrieve.
try:
change = drive.auth.service.changes().get(changeId=change_id).execute()
return change
except (errors.HttpError) as error:
web.app.logger.exception(error)
return None
# Deletes the local hashes database to force search for new folder names
def deleteDatabaseOnChange():
session.query(GdriveId).delete()
session.commit()