From 18970463e5147ad4fe36e15c1fe50a566d7f2d9e Mon Sep 17 00:00:00 2001 From: Nico Miguelino Date: Tue, 26 Nov 2024 07:36:04 -0800 Subject: [PATCH] api: create v2 endpoints in favor of the v1 endpoints currently used by Anthias from the Settings and home page (#2142) --- anthias_django/settings.py | 5 +- api/api_docs_filter_spec.py | 6 + api/tests.py | 4 +- api/urls.py | 64 +++++--- api/views/mixins.py | 270 ++++++++++++++++++++++++++++++++- api/views/v1.py | 287 ++++-------------------------------- api/views/v2.py | 44 +++++- static/js/anthias.coffee | 10 +- static/js/settings.coffee | 8 +- 9 files changed, 408 insertions(+), 290 deletions(-) create mode 100644 api/api_docs_filter_spec.py diff --git a/anthias_django/settings.py b/anthias_django/settings.py index 67d441b4b..a29141d3b 100644 --- a/anthias_django/settings.py +++ b/anthias_django/settings.py @@ -174,7 +174,10 @@ SPECTACULAR_SETTINGS = { 'TITLE': 'Anthias API', - 'VERSION': '1.2.0', + 'VERSION': '2.0.0', + 'PREPROCESSING_HOOKS': [ + 'api.api_docs_filter_spec.preprocessing_filter_spec' + ], } # `django-dbbackup` settings diff --git a/api/api_docs_filter_spec.py b/api/api_docs_filter_spec.py new file mode 100644 index 000000000..f9a1f0e90 --- /dev/null +++ b/api/api_docs_filter_spec.py @@ -0,0 +1,6 @@ +def preprocessing_filter_spec(endpoints): + filtered = [] + for (path, path_regex, method, callback) in endpoints: + if path.startswith("/api/v2"): + filtered.append((path, path_regex, method, callback)) + return filtered diff --git a/api/tests.py b/api/tests.py index c25e631b7..27e78f5e0 100644 --- a/api/tests.py +++ b/api/tests.py @@ -292,7 +292,7 @@ def test_device_info( self.assertEqual(data['viewlog'], 'Not yet implemented') @mock.patch( - 'api.views.v1.reboot_anthias.apply_async', + 'api.views.mixins.reboot_anthias.apply_async', side_effect=(lambda: None) ) def test_reboot(self, reboot_anthias_mock): @@ -303,7 +303,7 @@ def test_reboot(self, reboot_anthias_mock): self.assertEqual(reboot_anthias_mock.call_count, 1) @mock.patch( - 'api.views.v1.shutdown_anthias.apply_async', + 'api.views.mixins.shutdown_anthias.apply_async', side_effect=(lambda: None) ) def test_shutdown(self, shutdown_anthias_mock): diff --git a/api/urls.py b/api/urls.py index d952b7c64..7512dd5e2 100644 --- a/api/urls.py +++ b/api/urls.py @@ -2,16 +2,16 @@ from .views.v1 import ( AssetViewV1, AssetListViewV1, - AssetContentView, - FileAssetView, - PlaylistOrderView, - BackupView, - RecoverView, - AssetsControlView, + AssetContentViewV1, + FileAssetViewV1, + PlaylistOrderViewV1, + BackupViewV1, + RecoverViewV1, + AssetsControlViewV1, InfoView, - RebootView, - ShutdownView, - ViewerCurrentAssetView + RebootViewV1, + ShutdownViewV1, + ViewerCurrentAssetViewV1 ) from .views.v1_1 import ( AssetListViewV1_1, @@ -22,8 +22,16 @@ AssetViewV1_2 ) from .views.v2 import ( + AssetContentViewV2, + AssetsControlViewV2, AssetListViewV2, AssetViewV2, + BackupViewV2, + PlaylistOrderViewV2, + RecoverViewV2, + RebootViewV2, + ShutdownViewV2, + FileAssetViewV2 ) app_name = 'api' @@ -33,12 +41,12 @@ path('v1/assets', AssetListViewV1.as_view(), name='asset_list_v1'), path( 'v1/assets/order', - PlaylistOrderView.as_view(), + PlaylistOrderViewV1.as_view(), name='playlist_order_v1', ), path( 'v1/assets/control/', - AssetsControlView.as_view(), + AssetsControlViewV1.as_view(), name='assets_control_v1', ), path( @@ -48,18 +56,18 @@ ), path( 'v1/assets//content', - AssetContentView.as_view(), + AssetContentViewV1.as_view(), name='asset_content_v1', ), - path('v1/file_asset', FileAssetView.as_view(), name='file_asset_v1'), - path('v1/backup', BackupView.as_view(), name='backup_v1'), - path('v1/recover', RecoverView.as_view(), name='recover_v1'), + path('v1/file_asset', FileAssetViewV1.as_view(), name='file_asset_v1'), + path('v1/backup', BackupViewV1.as_view(), name='backup_v1'), + path('v1/recover', RecoverViewV1.as_view(), name='recover_v1'), path('v1/info', InfoView.as_view(), name='info_v1'), - path('v1/reboot', RebootView.as_view(), name='reboot_v1'), - path('v1/shutdown', ShutdownView.as_view(), name='shutdown_v1'), + path('v1/reboot', RebootViewV1.as_view(), name='reboot_v1'), + path('v1/shutdown', ShutdownViewV1.as_view(), name='shutdown_v1'), path( 'v1/viewer_current_asset', - ViewerCurrentAssetView.as_view(), + ViewerCurrentAssetViewV1.as_view(), name='viewer_current_asset_v1', ), @@ -81,9 +89,29 @@ # v2 endpoints path('v2/assets', AssetListViewV2.as_view(), name='asset_list_v2'), + path( + 'v2/assets/order', + PlaylistOrderViewV2.as_view(), + name='playlist_order_v2', + ), + path( + 'v2/assets/control/', + AssetsControlViewV2.as_view(), + name='assets_control_v2', + ), path( 'v2/assets/', AssetViewV2.as_view(), name='asset_detail_v2' ), + path('v2/backup', BackupViewV2.as_view(), name='backup_v2'), + path('v2/recover', RecoverViewV2.as_view(), name='recover_v2'), + path('v2/reboot', RebootViewV2.as_view(), name='reboot_v2'), + path('v2/shutdown', ShutdownViewV2.as_view(), name='shutdown_v2'), + path('v2/file_asset', FileAssetViewV2.as_view(), name='file_asset_v2'), + path( + 'v2/assets//content', + AssetContentViewV2.as_view(), + name='asset_content_v2', + ), ] diff --git a/api/views/mixins.py b/api/views/mixins.py index 88375ca1a..aad6ef3dc 100644 --- a/api/views/mixins.py +++ b/api/views/mixins.py @@ -1,11 +1,20 @@ -from drf_spectacular.utils import extend_schema +import uuid + +from base64 import b64encode +from inspect import cleandoc +from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes +from mimetypes import guess_type, guess_extension from rest_framework import status from rest_framework.response import Response +from rest_framework.views import APIView +from lib import backup_helper from lib.auth import authorized from anthias_app.models import Asset -from os import remove -from settings import settings +from api.helpers import save_active_assets_ordering +from celery_tasks import reboot_anthias, shutdown_anthias +from os import path, remove +from settings import settings, ZmqPublisher class DeleteAssetViewMixin: @@ -23,3 +32,258 @@ def delete(self, request, asset_id): asset.delete() return Response(status=status.HTTP_204_NO_CONTENT) + + +class BackupViewMixin(APIView): + @extend_schema( + summary='Create backup', + description=cleandoc(""" + Create a backup of the current Anthias instance, which + includes the following: + * current settings + * image and video assets + * asset metadata (e.g. name, duration, play order, status), + which is stored in a SQLite database + """), + responses={ + 201: { + 'type': 'string', + 'example': 'anthias-backup-2021-09-16T15-00-00.tar.gz', + 'description': 'Backup file name' + } + } + ) + @authorized + def post(self, request): + filename = backup_helper.create_backup(name=settings['player_name']) + return Response(filename, status=status.HTTP_201_CREATED) + + +class RecoverViewMixin(APIView): + @extend_schema( + summary='Recover from backup', + description=cleandoc(""" + Recover data from a backup file. The backup file must be + a `.tar.gz` file. + """), + request={ + 'multipart/form-data': { + 'type': 'object', + 'properties': { + 'backup_upload': { + 'type': 'string', + 'format': 'binary' + } + } + } + }, + responses={ + 200: { + 'type': 'string', + 'example': 'Recovery successful.', + } + }, + ) + @authorized + def post(self, request): + publisher = ZmqPublisher.get_instance() + file_upload = (request.data.get('backup_upload')) + filename = file_upload.name + + if guess_type(filename)[0] != 'application/x-tar': + raise Exception("Incorrect file extension.") + try: + publisher.send_to_viewer('stop') + location = path.join("static", filename) + + with open(location, 'wb') as f: + f.write(file_upload.read()) + + backup_helper.recover(location) + + return Response("Recovery successful.") + finally: + publisher.send_to_viewer('play') + + +class RebootViewMixin(APIView): + @extend_schema(summary='Reboot system') + @authorized + def post(self, request): + reboot_anthias.apply_async() + return Response(status=status.HTTP_200_OK) + + +class ShutdownViewMixin(APIView): + @extend_schema(summary='Shut down system') + @authorized + def post(self, request): + shutdown_anthias.apply_async() + return Response(status=status.HTTP_200_OK) + + +class FileAssetViewMixin(APIView): + @extend_schema( + summary='Upload file asset', + request={ + 'multipart/form-data': { + 'type': 'object', + 'properties': { + 'file_upload': { + 'type': 'string', + 'format': 'binary' + } + } + } + }, + responses={ + 200: { + 'type': 'object', + 'properties': { + 'uri': {'type': 'string'}, + 'ext': {'type': 'string'} + } + } + } + ) + @authorized + def post(self, request): + file_upload = request.data.get('file_upload') + filename = file_upload.name + file_type = guess_type(filename)[0] + + if not file_type: + raise Exception("Invalid file type.") + + if file_type.split('/')[0] not in ['image', 'video']: + raise Exception("Invalid file type.") + + file_path = path.join( + settings['assetdir'], + uuid.uuid5(uuid.NAMESPACE_URL, filename).hex, + ) + ".tmp" + + if 'Content-Range' in request.headers: + range_str = request.headers['Content-Range'] + start_bytes = int(range_str.split(' ')[1].split('-')[0]) + with open(file_path, 'ab') as f: + f.seek(start_bytes) + f.write(file_upload.read()) + else: + with open(file_path, 'wb') as f: + f.write(file_upload.read()) + + return Response({'uri': file_path, 'ext': guess_extension(file_type)}) + + +class AssetContentViewMixin(APIView): + @extend_schema( + summary='Get asset content', + description=cleandoc(""" + The content of the asset. + `type` can either be `file` or `url`. + + In case of a file, the fields `mimetype`, `filename`, and `content` + will be present. In case of a URL, the field `url` will be present. + """), + responses={ + 200: { + 'type': 'object', + 'properties': { + 'type': {'type': 'string'}, + 'url': {'type': 'string'}, + 'filename': {'type': 'string'}, + 'mimetype': {'type': 'string'}, + 'content': {'type': 'string'}, + } + } + } + ) + @authorized + def get(self, request, asset_id, format=None): + asset = Asset.objects.get(asset_id=asset_id) + + if path.isfile(asset.uri): + filename = asset.name + + with open(asset.uri, 'rb') as f: + content = f.read() + + mimetype = guess_type(filename)[0] + if not mimetype: + mimetype = 'application/octet-stream' + + result = { + 'type': 'file', + 'filename': filename, + 'content': b64encode(content).decode(), + 'mimetype': mimetype + } + else: + result = { + 'type': 'url', + 'url': asset.uri + } + + return Response(result) + + +class PlaylistOrderViewMixin(APIView): + @extend_schema( + summary='Update playlist order', + request={ + 'application/x-www-form-urlencoded': { + 'type': 'object', + 'properties': { + 'ids': { + 'type': 'string', + 'description': cleandoc( + """ + Comma-separated list of asset IDs in the order + they should be played. For example: + + `793406aa1fd34b85aa82614004c0e63a,1c5cfa719d1f4a9abae16c983a18903b,9c41068f3b7e452baf4dc3f9b7906595` + """ + ) + } + }, + } + } + ) + @authorized + def post(self, request): + asset_ids = request.data.get('ids', '').split(',') + save_active_assets_ordering(asset_ids) + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class AssetsControlViewMixin(APIView): + @extend_schema( + summary='Control asset playback', + description=cleandoc(""" + Use any of the following commands to control asset playback: + * `next` - Show the next asset + * `previous` - Show the previous asset + * `asset&{asset_id}` - Show the asset with the specified `asset_id` + """), + responses={ + 200: { + 'type': 'string', + 'example': 'Asset switched', + } + }, + parameters=[ + OpenApiParameter( + name='command', + location=OpenApiParameter.PATH, + type=OpenApiTypes.STR, + enum=['next', 'previous', 'asset&{asset_id}'], + ) + ] + ) + @authorized + def get(self, request, command): + publisher = ZmqPublisher.get_instance() + publisher.send_to_viewer(command) + return Response("Asset switched") diff --git a/api/views/v1.py b/api/views/v1.py index 8c5a748de..6b362ca75 100644 --- a/api/views/v1.py +++ b/api/views/v1.py @@ -1,6 +1,3 @@ -import uuid - -from inspect import cleandoc from rest_framework import serializers, status from rest_framework.response import Response from rest_framework.views import APIView @@ -12,31 +9,32 @@ from api.helpers import ( AssetCreationException, parse_request, - save_active_assets_ordering, ) -from base64 import b64encode -from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( extend_schema, inline_serializer, OpenApiExample, - OpenApiParameter, OpenApiRequest, ) from hurry.filesize import size -from lib import ( - backup_helper, - diagnostics -) +from lib import diagnostics from lib.auth import authorized from lib.github import is_up_to_date from lib.utils import connect_to_redis -from mimetypes import guess_type, guess_extension -from os import path, statvfs +from os import statvfs from anthias_app.models import Asset -from api.views.mixins import DeleteAssetViewMixin -from celery_tasks import reboot_anthias, shutdown_anthias -from settings import settings, ZmqCollector, ZmqPublisher +from api.views.mixins import ( + AssetContentViewMixin, + AssetsControlViewMixin, + BackupViewMixin, + DeleteAssetViewMixin, + FileAssetViewMixin, + PlaylistOrderViewMixin, + RebootViewMixin, + RecoverViewMixin, + ShutdownViewMixin, +) +from settings import ZmqCollector, ZmqPublisher r = connect_to_redis() @@ -115,56 +113,8 @@ def put(self, request, asset_id, format=None): return Response(AssetSerializer(asset).data) -class AssetContentView(APIView): - @extend_schema( - summary='Get asset content', - description=cleandoc(""" - The content of the asset. - `type` can either be `file` or `url`. - - In case of a file, the fields `mimetype`, `filename`, and `content` - will be present. In case of a URL, the field `url` will be present. - """), - responses={ - 200: { - 'type': 'object', - 'properties': { - 'type': {'type': 'string'}, - 'url': {'type': 'string'}, - 'filename': {'type': 'string'}, - 'mimetype': {'type': 'string'}, - 'content': {'type': 'string'}, - } - } - } - ) - @authorized - def get(self, request, asset_id, format=None): - asset = Asset.objects.get(asset_id=asset_id) - - if path.isfile(asset.uri): - filename = asset.name - - with open(asset.uri, 'rb') as f: - content = f.read() - - mimetype = guess_type(filename)[0] - if not mimetype: - mimetype = 'application/octet-stream' - - result = { - 'type': 'file', - 'filename': filename, - 'content': b64encode(content).decode(), - 'mimetype': mimetype - } - else: - result = { - 'type': 'url', - 'url': asset.uri - } - - return Response(result) +class AssetContentViewV1(AssetContentViewMixin): + pass class AssetListViewV1(APIView): @@ -206,191 +156,24 @@ def post(self, request, format=None): AssetSerializer(asset).data, status=status.HTTP_201_CREATED) -class FileAssetView(APIView): - @extend_schema( - summary='Upload file asset', - request={ - 'multipart/form-data': { - 'type': 'object', - 'properties': { - 'file_upload': { - 'type': 'string', - 'format': 'binary' - } - } - } - }, - responses={ - 200: { - 'type': 'object', - 'properties': { - 'uri': {'type': 'string'}, - 'ext': {'type': 'string'} - } - } - } - ) - @authorized - def post(self, request): - file_upload = request.data.get('file_upload') - filename = file_upload.name - file_type = guess_type(filename)[0] - - if not file_type: - raise Exception("Invalid file type.") - - if file_type.split('/')[0] not in ['image', 'video']: - raise Exception("Invalid file type.") - - file_path = path.join( - settings['assetdir'], - uuid.uuid5(uuid.NAMESPACE_URL, filename).hex, - ) + ".tmp" - - if 'Content-Range' in request.headers: - range_str = request.headers['Content-Range'] - start_bytes = int(range_str.split(' ')[1].split('-')[0]) - with open(file_path, 'ab') as f: - f.seek(start_bytes) - f.write(file_upload.read()) - else: - with open(file_path, 'wb') as f: - f.write(file_upload.read()) +class FileAssetViewV1(FileAssetViewMixin): + pass - return Response({'uri': file_path, 'ext': guess_extension(file_type)}) +class PlaylistOrderViewV1(PlaylistOrderViewMixin): + pass -class PlaylistOrderView(APIView): - @extend_schema( - summary='Update playlist order', - request={ - 'application/x-www-form-urlencoded': { - 'type': 'object', - 'properties': { - 'ids': { - 'type': 'string', - 'description': cleandoc( - """ - Comma-separated list of asset IDs in the order - they should be played. For example: - - `793406aa1fd34b85aa82614004c0e63a,1c5cfa719d1f4a9abae16c983a18903b,9c41068f3b7e452baf4dc3f9b7906595` - """ - ) - } - }, - } - } - ) - @authorized - def post(self, request): - asset_ids = request.data.get('ids', '').split(',') - save_active_assets_ordering(asset_ids) - return Response(status=status.HTTP_204_NO_CONTENT) +class BackupViewV1(BackupViewMixin): + pass -class BackupView(APIView): - @extend_schema( - summary='Create backup', - description=cleandoc(""" - Create a backup of the current Anthias instance, which - includes the following: - * current settings - * image and video assets - * asset metadata (e.g. name, duration, play order, status), - which is stored in a SQLite database - """), - responses={ - 201: { - 'type': 'string', - 'example': 'anthias-backup-2021-09-16T15-00-00.tar.gz', - 'description': 'Backup file name' - } - } - ) - @authorized - def post(self, request): - filename = backup_helper.create_backup(name=settings['player_name']) - return Response(filename, status=status.HTTP_201_CREATED) +class RecoverViewV1(RecoverViewMixin): + pass -class RecoverView(APIView): - @extend_schema( - summary='Recover from backup', - description=cleandoc(""" - Recover data from a backup file. The backup file must be - a `.tar.gz` file. - """), - request={ - 'multipart/form-data': { - 'type': 'object', - 'properties': { - 'backup_upload': { - 'type': 'string', - 'format': 'binary' - } - } - } - }, - responses={ - 200: { - 'type': 'string', - 'example': 'Recovery successful.', - } - }, - ) - @authorized - def post(self, request): - publisher = ZmqPublisher.get_instance() - file_upload = (request.data.get('backup_upload')) - filename = file_upload.name - - if guess_type(filename)[0] != 'application/x-tar': - raise Exception("Incorrect file extension.") - try: - publisher.send_to_viewer('stop') - location = path.join("static", filename) - - with open(location, 'wb') as f: - f.write(file_upload.read()) - - backup_helper.recover(location) - - return Response("Recovery successful.") - finally: - publisher.send_to_viewer('play') - - -class AssetsControlView(APIView): - @extend_schema( - summary='Control asset playback', - description=cleandoc(""" - Use any of the following commands to control asset playback: - * `next` - Show the next asset - * `previous` - Show the previous asset - * `asset&{asset_id}` - Show the asset with the specified `asset_id` - """), - responses={ - 200: { - 'type': 'string', - 'example': 'Asset switched', - } - }, - parameters=[ - OpenApiParameter( - name='command', - location=OpenApiParameter.PATH, - type=OpenApiTypes.STR, - enum=['next', 'previous', 'asset&{asset_id}'], - ) - ] - ) - @authorized - def get(self, request, command): - publisher = ZmqPublisher.get_instance() - publisher.send_to_viewer(command) - return Response("Asset switched") +class AssetsControlViewV1(AssetsControlViewMixin): + pass class InfoView(APIView): @@ -434,23 +217,15 @@ def get(self, request): }) -class RebootView(APIView): - @extend_schema(summary='Reboot system') - @authorized - def post(self, request): - reboot_anthias.apply_async() - return Response(status=status.HTTP_200_OK) +class RebootViewV1(RebootViewMixin): + pass -class ShutdownView(APIView): - @extend_schema(summary='Shut down system') - @authorized - def post(self, request): - shutdown_anthias.apply_async() - return Response(status=status.HTTP_200_OK) +class ShutdownViewV1(ShutdownViewMixin): + pass -class ViewerCurrentAssetView(APIView): +class ViewerCurrentAssetViewV1(APIView): @extend_schema( summary='Get current asset', description='Get the current asset being displayed on the screen', diff --git a/api/views/v2.py b/api/views/v2.py index f76b42a36..17c4e13ea 100644 --- a/api/views/v2.py +++ b/api/views/v2.py @@ -14,7 +14,17 @@ CreateAssetSerializerV2, UpdateAssetSerializerV2 ) -from api.views.mixins import DeleteAssetViewMixin +from api.views.mixins import ( + AssetContentViewMixin, + AssetsControlViewMixin, + BackupViewMixin, + DeleteAssetViewMixin, + PlaylistOrderViewMixin, + RebootViewMixin, + RecoverViewMixin, + ShutdownViewMixin, + FileAssetViewMixin +) from lib.auth import authorized @@ -125,3 +135,35 @@ def patch(self, request, asset_id): @authorized def put(self, request, asset_id): return self.update(request, asset_id, partial=False) + + +class BackupViewV2(BackupViewMixin): + pass + + +class RecoverViewV2(RecoverViewMixin): + pass + + +class RebootViewV2(RebootViewMixin): + pass + + +class ShutdownViewV2(ShutdownViewMixin): + pass + + +class FileAssetViewV2(FileAssetViewMixin): + pass + + +class AssetContentViewV2(AssetContentViewMixin): + pass + + +class PlaylistOrderViewV2(PlaylistOrderViewMixin): + pass + + +class AssetsControlViewV2(AssetsControlViewMixin): + pass diff --git a/static/js/anthias.coffee b/static/js/anthias.coffee index bd3409a4b..0f03d02f3 100644 --- a/static/js/anthias.coffee +++ b/static/js/anthias.coffee @@ -207,7 +207,7 @@ API.View.AddAssetView = class AddAssetView extends Backbone.View autoUpload: false sequentialUploads: true maxChunkSize: 5000000 #5 MB - url: 'api/v1/file_asset' + url: 'api/v2/file_asset' progressall: (e, data) => if data.loaded and data.total (@$ '.progress .bar').css 'width', "#{data.loaded / data.total * 100}%" add: (e, data) -> @@ -533,7 +533,7 @@ API.View.AssetRowView = class AssetRowView extends Backbone.View (@$ 'input, button').prop 'disabled', on download: (e) => - $.get '/api/v1/assets/' + @model.id + '/content', (result) -> + $.get '/api/v2/assets/' + @model.id + '/content', (result) -> switch result['type'] when 'url' window.open(result['url']) @@ -595,7 +595,7 @@ API.View.AssetsView = class AssetsView extends Backbone.View @collection.get(id).set('play_order', i) for id, i in active @collection.get(el.id).set('play_order', active.length) for el in (@$ '#inactive-assets tr').toArray() - $.post '/api/v1/assets/order', ids: ((@$ '#active-assets').sortable 'toArray').join ',' + $.post '/api/v2/assets/order', ids: ((@$ '#active-assets').sortable 'toArray').join ',' render: => @collection.sort() @@ -665,7 +665,7 @@ API.App = class App extends Backbone.View no previous: (e) -> - $.get '/api/v1/assets/control/previous' + $.get '/api/v2/assets/control/previous' next: (e) -> - $.get '/api/v1/assets/control/next' + $.get '/api/v2/assets/control/next' diff --git a/static/js/settings.coffee b/static/js/settings.coffee index d8c788f81..f546c9e7e 100644 --- a/static/js/settings.coffee +++ b/static/js/settings.coffee @@ -14,7 +14,7 @@ $().ready -> $.ajax({ method: "POST" - url: "/api/v1/backup" + url: "/api/v2/backup" timeout: 1800 * 1000 }) @@ -42,7 +42,7 @@ $().ready -> $("[name='backup_upload']").click() $("[name='backup_upload']").fileupload - url: "/api/v1/recover" + url: "/api/v2/recover" progressall: (e, data) -> if data.loaded and data.total valuenow = data.loaded/data.total*100 $(".progress .bar").css "width", valuenow + "%" @@ -74,7 +74,7 @@ $().ready -> $("#btn-reboot-system").click (e) -> if confirm "Are you sure you want to reboot your device?" - $.post "/api/v1/reboot" + $.post "/api/v2/reboot" .done (e) -> ($ "#request-error .alert").show() ($ "#request-error .alert").addClass "alert-success" @@ -91,7 +91,7 @@ $().ready -> $("#btn-shutdown-system").click (e) -> if confirm "Are you sure you want to shutdown your device?" - $.post "/api/v1/shutdown" + $.post "/api/v2/shutdown" .done (e) -> ($ "#request-error .alert").show() ($ "#request-error .alert").addClass "alert-success"