--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ width="64"
+ height="64"
+ xml:space="preserve"
+ sodipodi:docname="motioneye.svg"
+ inkscape:export-filename="/media/data/projects/motioneyeos/resources/motioneye-256x256.png"
+ inkscape:export-xdpi="360"
+ inkscape:export-ydpi="360"><metadata
+ id="metadata8"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs6" /><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1018"
+ id="namedview4"
+ showgrid="false"
+ inkscape:zoom="1"
+ inkscape:cx="31.099761"
+ inkscape:cy="76.252821"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="g10"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:showpageshadow="false"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-nodes="true"
+ inkscape:bbox-paths="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:snap-bbox-midpoints="true"
+ inkscape:object-paths="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-midpoints="true"
+ inkscape:snap-smooth-nodes="true" /><g
+ id="g10"
+ inkscape:groupmode="layer"
+ inkscape:label="ink_ext_XXXXXX"
+ transform="matrix(1.25,0,0,-1.25,0,64)"><path
+ style="fill:#3498db;fill-opacity:1;stroke:none"
+ d="m 32,6 c -5.264933,0 -10.15584,1.684955 -14.25,4.375 4.426748,0.474546 8.794165,1.616509 13.75,4.0625 5.014819,-2.5549 10.152222,-3.621792 14.78125,-4.15625 C 42.179879,7.5768125 37.280255,6 32,6 z m 17,5.59375 c -5.21085,0.03453 -12.330126,1.666208 -17.5,4.03125 -5.435679,-1.964064 -11.012887,-4.147356 -17.3125,-4 -0.419974,0.0099 -0.823167,0.03114 -1.25,0.0625 L 8,12.09375 l 4.6875,1.5625 c 6.429941,2.095118 12.084993,4.395085 17.0625,13.03125 1.154473,0 3.425649,5e-4 4.5,5e-4 4.957862,-8.587269 10.904708,-10.983331 17.03125,-13.03175 l 4.625,-1.5 -4.8125,-0.46875 C 50.437916,11.625136 49.744407,11.588818 49,11.59375 z M 12.65625,14.6875 C 8.5126665,19.293305 6,25.316864 6,32 6,46.359404 17.640596,58 32,58 46.359404,58 58,46.359404 58,32 58,25.341351 55.491547,19.287285 51.375,14.6875 49.406504,15.410957 47.442694,16.253379 45.5,17.34375 49.260018,18.43176 52,21.91463 52,26 c 0,4.945036 -4.027245,9 -9,9 -4.217962,0 -7.765819,-2.914031 -8.75,-6.8125 -1.243768,0 -3.31109,0 -4.5,0 C 28.774204,32.087724 25.219217,35 21,35 c -4.972755,0 -9.03125,-4.054964 -9.03125,-9 0,-4.133444 2.851912,-7.644 6.6875,-8.6875 -1.945667,-1.058808 -3.952016,-1.881003 -6,-2.625 z M 21,23 c -1.656854,0 -3,1.343146 -3,3 0,1.656854 1.343146,3 3,3 1.656854,0 3,-1.343146 3,-3 0,-1.656854 -1.343146,-3 -3,-3 z m 22,0 c -1.656854,0 -3,1.343146 -3,3 0,1.656854 1.343146,3 3,3 1.656854,0 3,-1.343146 3,-3 0,-1.656854 -1.343146,-3 -3,-3 z m -11,8 c 0.618234,2.299881 1.278866,4.581981 4,6 l -4,6 -4,-6 c 2.383634,-1.6888 3.483037,-3.758112 4,-6 z"
+ id="path3938"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="scccssccccccccccscsssccssccssccssssssssssccccc"
+ transform="matrix(0.8,0,0,-0.8,0,51.2)" /></g></svg>
\ No newline at end of file
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import httplib2
import json
import logging
import mimetypes
import urllib
import urllib2
-from oauth2client.client import OAuth2WebServerFlow, Credentials
-
import settings
_STATE_FILE_NAME = 'uploadservices.json'
-_services = {}
+_services = None
class UploadService(object):
def test_access(self):
return True
- def upload_file(self, filename):
- self.debug('uploading file "%s" to %s' % (filename, self))
+ def upload_file(self, target_dir, filename):
+ if target_dir:
+ target_dir = os.path.realpath(target_dir)
+ rel_filename = filename[len(target_dir):]
+
+ while rel_filename.startswith('/'):
+ rel_filename = rel_filename[1:]
+
+ self.debug('uploading file "[%s]/%s" to %s' % (target_dir, rel_filename, self))
+
+ else:
+ rel_filename = os.path.basename(filename)
+
+ self.debug('uploading file "%s" to %s' % (filename, self))
try:
st = os.stat(filename)
mime_type = mimetypes.guess_type(filename)[0] or 'image/jpeg'
self.debug('mime type of "%s" is "%s"' % (filename, mime_type))
- self.upload_data(filename, mime_type, data)
+ self.upload_data(rel_filename, mime_type, data)
+
+ self.debug('file "%s" successfully uploaded' % filename)
def upload_data(self, filename, mime_type, data):
pass
class GoogleDrive(UploadService):
NAME = 'gdrive'
+
+ AUTH_URL = 'https://accounts.google.com/o/oauth2/auth'
+ TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'
+
CLIENT_ID = '349038943026-m16svdadjrqc0c449u4qv71v1m1niu5o.apps.googleusercontent.com'
CLIENT_NOT_SO_SECRET = 'jjqbWmICpA0GvbhsJB3okX7s'
+
SCOPE = 'https://www.googleapis.com/auth/drive'
CHILDREN_URL = 'https://www.googleapis.com/drive/v2/files/%(parent_id)s/children?q=%(query)s'
CHILDREN_QUERY = "'%(parent_id)s' in parents and title = '%(child_name)s'"
UPLOAD_URL = 'https://www.googleapis.com/upload/drive/v2/files?uploadType=multipart'
+
BOUNDARY = 'motioneye_multipart_boundary'
MAX_FILE_SIZE = 128 * 1024 * 1024 # 128 MB
self._folder_id = None
def get_authorize_url(self):
- flow = OAuth2WebServerFlow(client_id=self.CLIENT_ID, client_secret=self.CLIENT_NOT_SO_SECRET,
- scope='https://www.googleapis.com/auth/drive', redirect_uri='urn:ietf:wg:oauth:2.0:oob')
+ query = {
+ 'scope': self.SCOPE,
+ 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob',
+ 'response_type': 'code',
+ 'client_id': self.CLIENT_ID,
+ 'access_type': 'offline'
+ }
- return flow.step1_get_authorize_url()
+ return self.AUTH_URL + '?' + urllib.urlencode(query)
def test_access(self):
try:
def dump(self):
return {
'location': self._location,
- 'credentials': self._credentials and json.loads(self._credentials.to_json()),
+ 'credentials': self._credentials,
'authorization_key': self._authorization_key,
'folder_id': self._folder_id
}
self._location = data['location']
self._folder_id = None # invalidate the folder
if 'credentials' in data:
- self._credentials = Credentials.new_from_json(json.dumps(data['credentials']))
+ self._credentials = data['credentials']
if 'authorization_key' in data:
self._authorization_key = data['authorization_key']
if 'folder_id' in data:
return self._folder_id
def _get_folder_id_by_path(self, path):
- path = [p.strip() for p in path.split('/') if p.strip()]
-
- parent_id = 'root'
- for name in path:
- parent_id = self._get_folder_id_by_name(parent_id, name)
+ if path and path != '/':
+ path = [p.strip() for p in path.split('/') if p.strip()]
+ parent_id = 'root'
+ for name in path:
+ parent_id = self._get_folder_id_by_name(parent_id, name)
+
+ return parent_id
- return parent_id
+ else: # root folder
+ return self._get_folder_id_by_name(None, 'root')
def _get_folder_id_by_name(self, parent_id, child_name):
- query = self.CHILDREN_QUERY % {'parent_id': parent_id, 'child_name': child_name}
- query = urllib.quote(query)
+ if parent_id:
+ query = self.CHILDREN_QUERY % {'parent_id': parent_id, 'child_name': child_name}
+ query = urllib.quote(query)
+
+ else:
+ query = ''
+
+ parent_id = parent_id or 'root'
+ # when requesting the id of the root folder, we perform a dummy request,
+ # event though we already know the id (which is "root"), to test the request
+
url = self.CHILDREN_URL % {'parent_id': parent_id, 'query': query}
response = self._request(url)
try:
except Exception:
self.error("response doesn't seem to be a valid json")
raise
+
+ if parent_id == 'root' and child_name == 'root':
+ return 'root'
items = response.get('items')
if not items:
raise Exception(msg)
if not self._credentials:
- self.debug('requesting access token')
- flow = self._get_oauth2_flow()
+ self.debug('requesting credentials')
try:
- self._credentials = flow.step2_exchange(self._authorization_key)
+ self._credentials = self._request_credentials(self._authorization_key)
save()
except Exception as e:
- self.error('failed to obtain access token: %s' % e)
+ self.error('failed to obtain credentials: %s' % e)
raise
headers = headers or {}
- headers['Authorization'] = 'Bearer %s' % self._credentials.access_token
+ headers['Authorization'] = 'Bearer %s' % self._credentials['access_token']
+
+ self.debug('requesting %s' % url)
+ request = urllib2.Request(url, data=body, headers=headers)
+ try:
+ response = urllib2.urlopen(request)
+
+ except urllib2.HTTPError as e:
+ if e.code == 401 and retry_auth: # unauthorized, access token may have expired
+ try:
+ self.debug('credentials have probably expired, refreshing them')
+ self._credentials = self._refresh_credentials(self._credentials['refresh_token'])
+ save()
+
+ # retry the request with refreshed credentials
+ return self._request(url, body, headers, retry_auth=False)
+
+ except Exception as e:
+ self.error('refreshing credentials failed')
+ raise
+
+ else:
+ try:
+ e = json.load(e)
+ msg = e['error']['message']
+
+ except Exception:
+ msg = str(e)
+
+ self.error('request failed: %s' % msg)
+ raise Exception(msg)
+
+ except Exception as e:
+ self.error('request failed: %s' % e)
+ raise
+
+ return response.read()
+
+ def _request_credentials(self, authorization_key):
+ headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+
+ body = {
+ 'code': authorization_key,
+ 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob',
+ 'client_id': self.CLIENT_ID,
+ 'client_secret': self.CLIENT_NOT_SO_SECRET,
+ 'scope': self.SCOPE,
+ 'grant_type': 'authorization_code'
+ }
+ body = urllib.urlencode(body)
+
+ request = urllib2.Request(self.TOKEN_URL, data=body, headers=headers)
+
+ try:
+ response = urllib2.urlopen(request)
+
+ except urllib2.HTTPError as e:
+ error = json.load(e)
+ raise Exception(error.get('error_description') or error.get('error') or str(e))
+
+ data = json.load(response)
+
+ return {
+ 'access_token': data['access_token'],
+ 'refresh_token': data['refresh_token']
+ }
+
+ def _refresh_credentials(self, refresh_token):
+ headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+
+ body = {
+ 'refresh_token': refresh_token,
+ 'client_id': self.CLIENT_ID,
+ 'client_secret': self.CLIENT_NOT_SO_SECRET,
+ 'grant_type': 'refresh_token'
+ }
+ body = urllib.urlencode(body)
+ request = urllib2.Request(self.TOKEN_URL, data=body, headers=headers)
+
+ try:
+ response = urllib2.urlopen(request)
+
+ except urllib2.HTTPError as e:
+ error = json.load(e)
+ raise Exception(error.get('error_description') or error.get('error') or str(e))
+
+ data = json.load(response)
+
+ return {
+ 'access_token': data['access_token'],
+ 'refresh_token': data.get('refresh_token', refresh_token)
+ }
+
+
+class Dropbox(UploadService):
+ NAME = 'dropbox'
+
+ AUTH_URL = 'https://www.dropbox.com/1/oauth2/authorize'
+ TOKEN_URL = 'https://api.dropboxapi.com/1/oauth2/token'
+
+ CLIENT_ID = 'dwiw710jz6r60pq'
+ CLIENT_NOT_SO_SECRET = '8jz75qo405ritd5'
+
+ LIST_FOLDER_URL = 'https://api.dropboxapi.com/2/files/list_folder'
+ UPLOAD_URL = 'https://content.dropboxapi.com/2/files/upload'
+
+ MAX_FILE_SIZE = 128 * 1024 * 1024 # 128 MB
+
+ def __init__(self, location=None, subfolders=True, authorization_key=None, credentials=None, **kwargs):
+ self._location = location
+ self._subfolders = subfolders
+ self._authorization_key = authorization_key
+ self._credentials = credentials
+
+ def get_authorize_url(self):
+ query = {
+ 'response_type': 'code',
+ 'client_id': self.CLIENT_ID
+ }
+
+ return self.AUTH_URL + '?' + urllib.urlencode(query)
+
+ def test_access(self):
+ body = {
+ 'path': self._clean_location(),
+ 'recursive': False,
+ 'include_media_info': False,
+ 'include_deleted': False
+ }
+
+ body = json.dumps(body)
+ headers = {'Content-Type': 'application/json'}
+
+ try:
+ self._request(self.LIST_FOLDER_URL, body, headers)
+ return True
+
+ except Exception as e:
+ msg = str(e)
+
+ # remove trailing punctuation
+ while msg and not msg[-1].isalnum():
+ msg = msg[:-1]
+
+ return msg
+
+ def upload_data(self, filename, mime_type, data):
+ metadata = {
+ 'path': os.path.join(self._clean_location(), filename),
+ 'mode': 'add',
+ 'autorename': True,
+ 'mute': False
+ }
+
+ headers = {
+ 'Content-Type': 'application/octet-stream',
+ 'Dropbox-API-Arg': json.dumps(metadata)
+ }
+
+ self._request(self.UPLOAD_URL, data, headers)
+
+ def dump(self):
+ return {
+ 'location': self._location,
+ 'subfolders': self._subfolders,
+ 'credentials': self._credentials,
+ 'authorization_key': self._authorization_key
+ }
+
+ def load(self, data):
+ if 'location' in data:
+ self._location = data['location']
+ if 'subfolders' in data:
+ self._subfolders = data['subfolders']
+ if 'credentials' in data:
+ self._credentials = data['credentials']
+ if 'authorization_key' in data:
+ self._authorization_key = data['authorization_key']
+
+ def _clean_location(self):
+ location = self._location
+ if location == '/':
+ return ''
+
+ if not location.startswith('/'):
+ location = '/' + location
+
+ return location
+
+ def _request(self, url, body=None, headers=None, retry_auth=True):
+ if not self._authorization_key:
+ msg = 'missing authorization key'
+ self.error(msg)
+ raise Exception(msg)
+
+ if not self._credentials:
+ self.debug('requesting credentials')
+ try:
+ self._credentials = self._request_credentials(self._authorization_key)
+ save()
+
+ except Exception as e:
+ self.error('failed to obtain credentials: %s' % e)
+ raise
+
+ headers = headers or {}
+ headers['Authorization'] = 'Bearer %s' % self._credentials['access_token']
+
self.debug('requesting %s' % url)
request = urllib2.Request(url, data=body, headers=headers)
try:
except urllib2.HTTPError as e:
if e.code == 401 and retry_auth: # unauthorized, access token may have expired
try:
- self.debug('access token has probably expired, refreshing it')
- self._credentials.refresh(httplib2.Http())
+ self.debug('credentials have probably expired, refreshing them')
+ self._credentials = self._refresh_credentials(self._credentials['refresh_token'])
save()
+
+ # retry the request with refreshed credentials
self._request(url, body, headers, retry_auth=False)
except Exception as e:
- self.error('refreshing access token failed')
+ self.error('refreshing credentials failed')
raise
-
+
+ elif str(e).count('not_found'):
+ msg = 'folder "%s" not found' % self._location
+ self.error(msg)
+ raise Exception(msg)
+
else:
self.error('request failed: %s' % e)
raise
raise
return response.read()
+
+ def _request_credentials(self, authorization_key):
+ headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+
+ body = {
+ 'code': authorization_key,
+ 'client_id': self.CLIENT_ID,
+ 'client_secret': self.CLIENT_NOT_SO_SECRET,
+ 'grant_type': 'authorization_code'
+ }
+ body = urllib.urlencode(body)
- def _get_oauth2_flow(self):
- return OAuth2WebServerFlow(client_id=self.CLIENT_ID, client_secret=self.CLIENT_NOT_SO_SECRET,
- scope=self.SCOPE, redirect_uri='urn:ietf:wg:oauth:2.0:oob')
-
+ request = urllib2.Request(self.TOKEN_URL, data=body, headers=headers)
+
+ try:
+ response = urllib2.urlopen(request)
+
+ except urllib2.HTTPError as e:
+ error = json.load(e)
+ raise Exception(error.get('error_description') or error.get('error') or str(e))
+
+ data = json.load(response)
+
+ return {
+ 'access_token': data['access_token']
+ }
+
def get(camera_id, name, create=True):
+ if _services is None:
+ load()
+
camera_id = str(camera_id)
service = _services.get(camera_id, {}).get(name)
if not service and create:
def load():
+ global _services
+
+ _services = {}
+
file_path = os.path.join(settings.CONF_PATH, _STATE_FILE_NAME)
if os.path.exists(file_path):
def save():
+ if _services is None:
+ return
+
file_path = os.path.join(settings.CONF_PATH, _STATE_FILE_NAME)
logging.debug('saving upload services state to "%s"...' % file_path)
file.close()
-def upload_media_file(camera_id, service_name, filename):
+def upload_media_file(camera_id, target_dir, service_name, filename):
+ # force a load from file with every upload,
+ # as settings might have changed
+ load()
+
service = get(camera_id, service_name, create=False)
if not service:
return logging.error('service "%s" not initialized for camera with id %s' % (service_name, camera_id))
try:
- service.upload_file(filename)
+ service.upload_file(target_dir, filename)
except Exception as e:
logging.error('failed to upload file "%s" with service %s: %s' % (filename, service, e))