diff --git a/apps/common/drf/renders/__init__.py b/apps/common/drf/renders/__init__.py index 671c86586..f99b13586 100644 --- a/apps/common/drf/renders/__init__.py +++ b/apps/common/drf/renders/__init__.py @@ -1 +1,13 @@ -from .csv import * \ No newline at end of file +from rest_framework import renderers + +from .csv import * + + +class PassthroughRenderer(renderers.BaseRenderer): + """ + Return data as-is. View should supply a Response. + """ + media_type = '' + format = '' + def render(self, data, accepted_media_type=None, renderer_context=None): + return data diff --git a/apps/terminal/api/session.py b/apps/terminal/api/session.py index a8ed747c2..f340147c3 100644 --- a/apps/terminal/api/session.py +++ b/apps/terminal/api/session.py @@ -1,15 +1,25 @@ # -*- coding: utf-8 -*- # -from django.utils.translation import ugettext as _ +import os +import tarfile + from django.shortcuts import get_object_or_404, reverse +from django.utils.translation import ugettext as _ +from django.utils.encoding import escape_uri_path +from django.http import FileResponse, HttpResponse from django.core.files.storage import default_storage from rest_framework import viewsets, views from rest_framework.response import Response +from rest_framework.decorators import action +from common.utils import model_to_json +from .. import utils +from common.const.http import GET from common.utils import is_uuid, get_logger, get_object_or_none from common.mixins.api import AsyncApiMixin from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser from common.drf.filters import DatetimeRangeFilter +from common.drf.renders import PassthroughRenderer from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import tmp_to_root_org, tmp_to_org from users.models import User @@ -41,6 +51,44 @@ class SessionViewSet(OrgBulkModelViewSet): ] extra_filter_backends = [DatetimeRangeFilter] + @staticmethod + def prepare_offline_file(session, local_path): + replay_path = default_storage.path(local_path) + current_dir = os.getcwd() + dir_path = os.path.dirname(replay_path) + replay_filename = os.path.basename(replay_path) + meta_filename = '{}.json'.format(session.id) + offline_filename = '{}.tar'.format(session.id) + os.chdir(dir_path) + + with open(meta_filename, 'wt') as f: + f.write(model_to_json(session)) + + with tarfile.open(offline_filename, 'w') as f: + f.add(replay_filename) + f.add(meta_filename) + file = open(offline_filename, 'rb') + os.chdir(current_dir) + return file + + @action(methods=[GET], detail=True, renderer_classes=(PassthroughRenderer,), url_path='replay/download', url_name='replay-download') + def download(self, request, *args, **kwargs): + session = self.get_object() + local_path, url = utils.get_session_replay_url(session) + if local_path is None: + error = url + return HttpResponse(error) + file = self.prepare_offline_file(session, local_path) + + response = FileResponse(file) + response['Content-Type'] = 'application/octet-stream' + # 这里要注意哦,网上查到的方法都是response['Content-Disposition']='attachment;filename="filename.py"', + # 但是如果文件名是英文名没问题,如果文件名包含中文,下载下来的文件名会被改为url中的path。 + filename = escape_uri_path('{}.tar'.format(session.id)) + disposition = "attachment; filename*=UTF-8''{}".format(filename) + response["Content-Disposition"] = disposition + return response + def filter_queryset(self, queryset): queryset = super().filter_queryset(queryset) # 解决guacamole更新session时并发导致幽灵会话的问题 @@ -95,7 +143,7 @@ class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet): if session.protocol in ('rdp', 'vnc'): tp = 'guacamole' - download_url = reverse('terminal:session-replay-download', kwargs={'pk': session.id}) + download_url = reverse('api-terminal:session-replay-download', kwargs={'pk': session.id}) data = { 'type': tp, 'src': url, 'user': session.user, 'asset': session.asset,