feat: 支持文件上传下载备份 (#10438)

* feat: 支持文件上传下载备份

* perf: 抽离replay和ftpfile存储代码

* perf: FTPLog增加session字段

* fix: 修改变量名
pull/10657/head
jiangweidong 2023-06-08 18:04:07 +08:00 committed by GitHub
parent 271ec1bfe0
commit 2837dcf40e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 294 additions and 103 deletions

View File

@ -1,30 +1,47 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import os
from importlib import import_module from importlib import import_module
from django.conf import settings from django.conf import settings
from django.shortcuts import get_object_or_404
from django.db.models import F, Value, CharField, Q from django.db.models import F, Value, CharField, Q
from django.http import HttpResponse, FileResponse
from django.utils.encoding import escape_uri_path
from rest_framework import generics from rest_framework import generics
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.decorators import action
from common.api import AsyncApiMixin
from common.drf.filters import DatetimeRangeFilter from common.drf.filters import DatetimeRangeFilter
from common.permissions import IsServiceAccount
from common.plugins.es import QuerySet as ESQuerySet from common.plugins.es import QuerySet as ESQuerySet
from common.utils import is_uuid from common.utils import is_uuid, get_logger, lazyproperty
from common.utils import lazyproperty from common.const.http import GET, POST
from common.storage.ftp_file import FTPFileStorageHandler
from orgs.mixins.api import OrgReadonlyModelViewSet, OrgModelViewSet from orgs.mixins.api import OrgReadonlyModelViewSet, OrgModelViewSet
from orgs.utils import current_org, tmp_to_root_org from orgs.utils import current_org, tmp_to_root_org
from orgs.models import Organization from orgs.models import Organization
from rbac.permissions import RBACPermission
from terminal.models import default_storage
from users.models import User from users.models import User
from .backends import TYPE_ENGINE_MAPPING from .backends import TYPE_ENGINE_MAPPING
from .const import ActivityChoices from .const import ActivityChoices
from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog, ActivityLog, JobLog from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog, ActivityLog, JobLog
from .serializers import FTPLogSerializer, UserLoginLogSerializer, JobLogSerializer
from .serializers import ( from .serializers import (
FTPLogSerializer, UserLoginLogSerializer, JobLogSerializer,
OperateLogSerializer, OperateLogActionDetailSerializer, OperateLogSerializer, OperateLogActionDetailSerializer,
PasswordChangeLogSerializer, ActivityUnionLogSerializer, PasswordChangeLogSerializer, ActivityUnionLogSerializer,
FileSerializer
) )
logger = get_logger(__name__)
class JobAuditViewSet(OrgReadonlyModelViewSet): class JobAuditViewSet(OrgReadonlyModelViewSet):
model = JobLog model = JobLog
extra_filter_backends = [DatetimeRangeFilter] extra_filter_backends = [DatetimeRangeFilter]
@ -47,7 +64,49 @@ class FTPLogViewSet(OrgModelViewSet):
filterset_fields = ['user', 'asset', 'account', 'filename'] filterset_fields = ['user', 'asset', 'account', 'filename']
search_fields = filterset_fields search_fields = filterset_fields
ordering = ['-date_start'] ordering = ['-date_start']
http_method_names = ['post', 'get', 'head', 'options'] http_method_names = ['post', 'get', 'head', 'options', 'patch']
rbac_perms = {
'download': 'audits.view_ftplog',
}
def get_storage(self):
return FTPFileStorageHandler(self.get_object())
@action(
methods=[GET], detail=True, permission_classes=[RBACPermission, ],
url_path='file/download'
)
def download(self, request, *args, **kwargs):
ftp_log = self.get_object()
ftp_storage = self.get_storage()
local_path, url_or_err = ftp_storage.get_file_path_url()
if local_path is None:
return HttpResponse(url_or_err)
file = open(default_storage.path(local_path), 'rb')
response = FileResponse(file)
response['Content-Type'] = 'application/octet-stream'
filename = escape_uri_path(ftp_log.filename)
response["Content-Disposition"] = "attachment; filename*=UTF-8''{}".format(filename)
return response
@action(methods=[POST], detail=True, permission_classes=[IsServiceAccount, ], serializer_class=FileSerializer)
def upload(self, request, *args, **kwargs):
ftp_log = self.get_object()
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
file = serializer.validated_data['file']
name, err = ftp_log.save_file_to_storage(file)
if not name:
msg = "Failed save file `{}`: {}".format(ftp_log.id, err)
logger.error(msg)
return Response({'msg': str(err)}, status=400)
url = default_storage.url(name)
return Response({'url': url}, status=201)
else:
msg = 'Upload data invalid: {}'.format(serializer.errors)
logger.error(msg)
return Response({'msg': serializer.errors}, status=401)
class UserLoginCommonMixin: class UserLoginCommonMixin:

View File

@ -16,6 +16,7 @@ class OperateChoices(TextChoices):
rename = "rename", _("Rename") rename = "rename", _("Rename")
symlink = "symlink", _("Symlink") symlink = "symlink", _("Symlink")
download = "download", _("Download") download = "download", _("Download")
rename_dir = "rename_dir", _("Rename dir")
class ActionChoices(TextChoices): class ActionChoices(TextChoices):

View File

@ -0,0 +1,29 @@
# Generated by Django 3.2.17 on 2023-06-05 07:55
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('audits', '0021_auto_20230207_0857'),
]
operations = [
migrations.AddField(
model_name='ftplog',
name='has_file',
field=models.BooleanField(default=False, verbose_name='File Record'),
),
migrations.AddField(
model_name='ftplog',
name='session',
field=models.CharField(default=uuid.uuid4, max_length=36, verbose_name='Session'),
),
migrations.AlterField(
model_name='ftplog',
name='operate',
field=models.CharField(choices=[('mkdir', 'Mkdir'), ('rmdir', 'Rmdir'), ('delete', 'Delete'), ('upload', 'Upload'), ('rename', 'Rename'), ('symlink', 'Symlink'), ('download', 'Download'), ('rename_dir', 'Rename dir')], max_length=16, verbose_name='Operate'),
),
]

View File

@ -1,7 +1,9 @@
import os
import uuid import uuid
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext, ugettext_lazy as _ from django.utils.translation import gettext, ugettext_lazy as _
@ -10,6 +12,7 @@ from common.utils import lazyproperty
from ops.models import JobExecution from ops.models import JobExecution
from orgs.mixins.models import OrgModelMixin, Organization from orgs.mixins.models import OrgModelMixin, Organization
from orgs.utils import current_org from orgs.utils import current_org
from terminal.models import default_storage
from .const import ( from .const import (
OperateChoices, OperateChoices,
ActionChoices, ActionChoices,
@ -40,6 +43,8 @@ class JobLog(JobExecution):
class FTPLog(OrgModelMixin): class FTPLog(OrgModelMixin):
upload_to = 'FTP_FILES'
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_("User")) user = models.CharField(max_length=128, verbose_name=_("User"))
remote_addr = models.CharField( remote_addr = models.CharField(
@ -53,10 +58,27 @@ class FTPLog(OrgModelMixin):
filename = models.CharField(max_length=1024, verbose_name=_("Filename")) filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
is_success = models.BooleanField(default=True, verbose_name=_("Success")) is_success = models.BooleanField(default=True, verbose_name=_("Success"))
date_start = models.DateTimeField(auto_now_add=True, verbose_name=_("Date start")) date_start = models.DateTimeField(auto_now_add=True, verbose_name=_("Date start"))
has_file = models.BooleanField(default=False, verbose_name=_("File Record"))
session = models.CharField(max_length=36, verbose_name=_("Session"), default=uuid.uuid4)
class Meta: class Meta:
verbose_name = _("File transfer log") verbose_name = _("File transfer log")
@property
def filepath(self):
return os.path.join(self.upload_to, self.date_start.strftime('%Y-%m-%d'), str(self.id))
def save_file_to_storage(self, file):
try:
name = default_storage.save(self.filepath, file)
except OSError as e:
return None, e
if settings.SERVER_REPLAY_STORAGE:
from .tasks import upload_ftp_file_to_external_storage
upload_ftp_file_to_external_storage.delay(str(self.id), file.name)
return name, None
class OperateLog(OrgModelMixin): class OperateLog(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)

View File

@ -37,8 +37,8 @@ class FTPLogSerializer(serializers.ModelSerializer):
fields_mini = ["id"] fields_mini = ["id"]
fields_small = fields_mini + [ fields_small = fields_mini + [
"user", "remote_addr", "asset", "account", "user", "remote_addr", "asset", "account",
"org_id", "operate", "filename", "is_success", "org_id", "operate", "filename", "date_start",
"date_start", "is_success", "has_file",
] ]
fields = fields_small fields = fields_small
@ -158,3 +158,7 @@ class ActivityUnionLogSerializer(serializers.Serializer):
api_to_ui=True, is_audit=True api_to_ui=True, is_audit=True
) )
return detail_url return detail_url
class FileSerializer(serializers.Serializer):
file = serializers.FileField(allow_empty_file=True)

View File

@ -11,13 +11,16 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.utils import get_log_keep_day, get_logger from common.utils import get_log_keep_day, get_logger
from common.storage.ftp_file import FTPFileStorageHandler
from ops.celery.decorator import ( from ops.celery.decorator import (
register_as_period_task, after_app_shutdown_clean_periodic register_as_period_task, after_app_shutdown_clean_periodic
) )
from ops.models import CeleryTaskExecution from ops.models import CeleryTaskExecution
from terminal.models import Session, Command from terminal.models import Session, Command
from terminal.backends import server_replay_storage
from .models import UserLoginLog, OperateLog, FTPLog, ActivityLog from .models import UserLoginLog, OperateLog, FTPLog, ActivityLog
logger = get_logger(__name__) logger = get_logger(__name__)
@ -46,7 +49,15 @@ def clean_ftp_log_period():
now = timezone.now() now = timezone.now()
days = get_log_keep_day('FTP_LOG_KEEP_DAYS') days = get_log_keep_day('FTP_LOG_KEEP_DAYS')
expired_day = now - datetime.timedelta(days=days) expired_day = now - datetime.timedelta(days=days)
file_store_dir = os.path.join(default_storage.base_location, 'ftp_file')
FTPLog.objects.filter(date_start__lt=expired_day).delete() FTPLog.objects.filter(date_start__lt=expired_day).delete()
command = "find %s -mtime +%s -exec rm -f {} \\;" % (
file_store_dir, days
)
subprocess.call(command, shell=True)
command = "find %s -type d -empty -delete;" % file_store_dir
subprocess.call(command, shell=True)
logger.info("Clean FTP file done")
def clean_celery_tasks_period(): def clean_celery_tasks_period():
@ -98,3 +109,27 @@ def clean_audits_log_period():
clean_activity_log_period() clean_activity_log_period()
clean_celery_tasks_period() clean_celery_tasks_period()
clean_expired_session_period() clean_expired_session_period()
@shared_task(verbose_name=_('Upload FTP file to external storage'))
def upload_ftp_file_to_external_storage(ftp_log_id, file_name):
logger.info(f'Start upload FTP file record to external storage: {ftp_log_id} - {file_name}')
ftp_log = FTPLog.objects.filter(id=ftp_log_id).first()
if not ftp_log:
logger.error(f'FTP db item not found: {ftp_log_id}')
return
ftp_storage = FTPFileStorageHandler(ftp_log)
local_path, url_or_err = ftp_storage.find_local()
if not local_path:
logger.error(f'FTP file record not found, may be upload error. file name: {file_name}')
return
abs_path = default_storage.path(local_path)
ok, err = server_replay_storage.upload(abs_path, ftp_log.filepath)
if not ok:
logger.error(f'Session file record upload to external error: {err}')
return
try:
default_storage.delete(local_path)
except:
pass
return

View File

@ -1,7 +1,5 @@
import codecs import codecs
import copy import copy
import csv
from itertools import chain from itertools import chain
from datetime import datetime from datetime import datetime

View File

View File

@ -0,0 +1,65 @@
import os
import jms_storage
from django.conf import settings
from terminal.models import default_storage, ReplayStorage
from common.utils import get_logger, make_dirs
logger = get_logger(__name__)
class BaseStorageHandler(object):
NAME = ''
def __init__(self, obj):
self.obj = obj
def get_file_path(self, **kwargs):
# return remote_path, local_path
raise NotImplementedError
def find_local(self):
raise NotImplementedError
def download(self):
replay_storages = ReplayStorage.objects.all()
configs = {
storage.name: storage.config
for storage in replay_storages
if not storage.type_null_or_server
}
if settings.SERVER_REPLAY_STORAGE:
configs['SERVER_REPLAY_STORAGE'] = settings.SERVER_REPLAY_STORAGE
if not configs:
msg = f"Not found {self.NAME} file, and not remote storage set"
return None, msg
storage = jms_storage.get_multi_object_storage(configs)
remote_path, local_path = self.get_file_path(storage=storage)
if not remote_path:
msg = f'Not found {self.NAME} file'
logger.error(msg)
return None, msg
# 保存到storage的路径
target_path = os.path.join(default_storage.base_location, local_path)
target_dir = os.path.dirname(target_path)
if not os.path.isdir(target_dir):
make_dirs(target_dir, exist_ok=True)
ok, err = storage.download(remote_path, target_path)
if not ok:
msg = f'Failed download {self.NAME} file: {err}'
logger.error(msg)
return None, msg
url = default_storage.url(local_path)
return local_path, url
def get_file_path_url(self):
local_path, url = self.find_local()
if local_path is None:
local_path, url = self.download()
return local_path, url

View File

@ -0,0 +1,17 @@
from terminal.models import default_storage
from .base import BaseStorageHandler
class FTPFileStorageHandler(BaseStorageHandler):
NAME = 'FTP'
def get_file_path(self, **kwargs):
return self.obj.filepath, self.obj.filepath
def find_local(self):
local_path = self.obj.filepath
# 去default storage中查找
if default_storage.exists(local_path):
url = default_storage.url(local_path)
return local_path, url
return None, None

View File

@ -0,0 +1,31 @@
from itertools import chain
from terminal.models import default_storage
from .base import BaseStorageHandler
class ReplayStorageHandler(BaseStorageHandler):
NAME = 'REPLAY'
def get_file_path(self, **kwargs):
storage = kwargs['storage']
# 获取外部存储路径名
session_path = self.obj.find_ok_relative_path_in_storage(storage)
if not session_path:
return None
# 通过外部存储路径名后缀,构造真实的本地存储路径
return session_path, self.obj.get_local_path_by_relative_path(session_path)
def find_local(self):
# 存在外部存储上,所有可能的路径名
session_paths = self.obj.get_all_possible_relative_path()
# 存在本地存储上,所有可能的路径名
local_paths = self.obj.get_all_possible_local_path()
for _local_path in chain(session_paths, local_paths):
if default_storage.exists(_local_path):
url = default_storage.url(_local_path)
return _local_path, url
return None, f'{self.NAME} not found.'

View File

@ -548,6 +548,9 @@ class Config(dict):
# Applet 等软件的下载地址 # Applet 等软件的下载地址
'APPLET_DOWNLOAD_HOST': '', 'APPLET_DOWNLOAD_HOST': '',
# FTP 文件上传下载备份阈值,单位(M)当值小于等于0时不备份
'FTP_FILE_MAX_STORE': 100,
} }
def __init__(self, *args): def __init__(self, *args):

View File

@ -29,6 +29,7 @@ DEFAULT_TERMINAL_REPLAY_STORAGE = {
}, },
} }
TERMINAL_REPLAY_STORAGE = CONFIG.TERMINAL_REPLAY_STORAGE TERMINAL_REPLAY_STORAGE = CONFIG.TERMINAL_REPLAY_STORAGE
FTP_FILE_MAX_STORE = CONFIG.FTP_FILE_MAX_STORE
# Security settings # Security settings
SECURITY_MFA_AUTH = CONFIG.SECURITY_MFA_AUTH SECURITY_MFA_AUTH = CONFIG.SECURITY_MFA_AUTH

View File

@ -91,7 +91,7 @@ exclude_permissions = (
('audits', 'activitylog', 'add,delete,change', 'activitylog'), ('audits', 'activitylog', 'add,delete,change', 'activitylog'),
('audits', 'passwordchangelog', 'add,change,delete', 'passwordchangelog'), ('audits', 'passwordchangelog', 'add,change,delete', 'passwordchangelog'),
('audits', 'userloginlog', 'add,change,delete,change', 'userloginlog'), ('audits', 'userloginlog', 'add,change,delete,change', 'userloginlog'),
('audits', 'ftplog', 'change,delete', 'ftplog'), ('audits', 'ftplog', 'delete', 'ftplog'),
('tickets', 'ticketassignee', '*', 'ticketassignee'), ('tickets', 'ticketassignee', '*', 'ticketassignee'),
('tickets', 'ticketflow', 'add,delete', 'ticketflow'), ('tickets', 'ticketflow', 'add,delete', 'ticketflow'),
('tickets', 'comment', '*', '*'), ('tickets', 'comment', '*', '*'),

View File

@ -22,15 +22,13 @@ from common.drf.renders import PassthroughRenderer
from common.api import AsyncApiMixin from common.api import AsyncApiMixin
from common.utils import data_to_json, is_uuid from common.utils import data_to_json, is_uuid
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from common.storage.replay import ReplayStorageHandler
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import tmp_to_root_org, tmp_to_org from orgs.utils import tmp_to_root_org, tmp_to_org
from terminal import serializers from terminal import serializers
from terminal.models import Session from terminal.models import Session
from terminal.utils import ( from terminal.utils import is_session_approver
find_session_replay_local, download_session_replay,
is_session_approver, get_session_replay_url
)
from terminal.permissions import IsSessionAssignee from terminal.permissions import IsSessionAssignee
from users.models import User from users.models import User
@ -112,20 +110,23 @@ class SessionViewSet(OrgBulkModelViewSet):
os.chdir(current_dir) os.chdir(current_dir)
return file return file
def get_storage(self):
return ReplayStorageHandler(self.get_object())
@action(methods=[GET], detail=True, renderer_classes=(PassthroughRenderer,), url_path='replay/download', @action(methods=[GET], detail=True, renderer_classes=(PassthroughRenderer,), url_path='replay/download',
url_name='replay-download') url_name='replay-download')
def download(self, request, *args, **kwargs): def download(self, request, *args, **kwargs):
session = self.get_object() storage = self.get_storage()
local_path, url = get_session_replay_url(session) local_path, url_or_err = storage.get_file_path_url()
if local_path is None: if local_path is None:
return Response({"error": url}, status=404) return Response({'error': url_or_err}, status=404)
file = self.prepare_offline_file(session, local_path)
file = self.prepare_offline_file(storage.obj, local_path)
response = FileResponse(file) response = FileResponse(file)
response['Content-Type'] = 'application/octet-stream' response['Content-Type'] = 'application/octet-stream'
# 这里要注意哦网上查到的方法都是response['Content-Disposition']='attachment;filename="filename.py"', # 这里要注意哦网上查到的方法都是response['Content-Disposition']='attachment;filename="filename.py"',
# 但是如果文件名是英文名没问题如果文件名包含中文下载下来的文件名会被改为url中的path。 # 但是如果文件名是英文名没问题如果文件名包含中文下载下来的文件名会被改为url中的path。
filename = escape_uri_path('{}.tar'.format(session.id)) filename = escape_uri_path('{}.tar'.format(storage.obj.id))
disposition = "attachment; filename*=UTF-8''{}".format(filename) disposition = "attachment; filename*=UTF-8''{}".format(filename)
response["Content-Disposition"] = disposition response["Content-Disposition"] = disposition
return response return response
@ -208,13 +209,12 @@ class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
session_id = kwargs.get('pk') session_id = kwargs.get('pk')
session = get_object_or_404(Session, id=session_id) session = get_object_or_404(Session, id=session_id)
local_path, url = find_session_replay_local(session)
if not local_path: storage = ReplayStorageHandler(session)
local_path, url = download_session_replay(session) local_path, url_or_err = storage.get_file_path_url()
if not local_path: if url_or_err:
return Response({"error": url}, status=404) return Response({"error": url_or_err}, status=404)
data = self.get_replay_data(session, url) data = self.get_replay_data(session, url_or_err)
return Response(data) return Response(data)

View File

@ -129,7 +129,8 @@ class Terminal(StorageMixin, TerminalStatusMixin, JMSBaseModel):
configs.update(self.get_login_title_setting()) configs.update(self.get_login_title_setting())
configs.update({ configs.update({
'SECURITY_MAX_IDLE_TIME': settings.SECURITY_MAX_IDLE_TIME, 'SECURITY_MAX_IDLE_TIME': settings.SECURITY_MAX_IDLE_TIME,
'SECURITY_SESSION_SHARE': settings.SECURITY_SESSION_SHARE 'SECURITY_SESSION_SHARE': settings.SECURITY_SESSION_SHARE,
'FTP_FILE_MAX_STORE': settings.FTP_FILE_MAX_STORE,
}) })
return configs return configs

View File

@ -10,6 +10,7 @@ from django.core.files.storage import default_storage
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.storage.replay import ReplayStorageHandler
from ops.celery.decorator import ( from ops.celery.decorator import (
register_as_period_task, after_app_ready_start, register_as_period_task, after_app_ready_start,
after_app_shutdown_clean_periodic after_app_shutdown_clean_periodic
@ -23,7 +24,6 @@ from .models import (
AppletHost, ReplayStorage, CommandStorage AppletHost, ReplayStorage, CommandStorage
) )
from .notifications import StorageConnectivityMessage from .notifications import StorageConnectivityMessage
from .utils import find_session_replay_local
CACHE_REFRESH_INTERVAL = 10 CACHE_REFRESH_INTERVAL = 10
RUNNING = False RUNNING = False
@ -66,7 +66,8 @@ def upload_session_replay_to_external_storage(session_id):
logger.error(f'Session db item not found: {session_id}') logger.error(f'Session db item not found: {session_id}')
return return
local_path, foobar = find_session_replay_local(session) replay_storage = ReplayStorageHandler(session)
local_path, url_or_err = replay_storage.find_local()
if not local_path: if not local_path:
logger.error(f'Session replay not found, may be upload error: {local_path}') logger.error(f'Session replay not found, may be upload error: {local_path}')
return return

View File

@ -1,4 +1,3 @@
from .components import * from .components import *
from .common import * from .common import *
from .session_replay import *
from .db_port_mapper import * from .db_port_mapper import *

View File

@ -1,75 +0,0 @@
# -*- coding: utf-8 -*-
#
import os
from itertools import groupby, chain
from django.conf import settings
from django.core.files.storage import default_storage
import jms_storage
from common.utils import get_logger, make_dirs
from ..models import ReplayStorage
logger = get_logger(__name__)
def find_session_replay_local(session):
# 存在外部存储上,所有可能的路径名
session_paths = session.get_all_possible_relative_path()
# 存在本地存储上,所有可能的路径名
local_paths = session.get_all_possible_local_path()
for _local_path in chain(session_paths, local_paths):
if default_storage.exists(_local_path):
url = default_storage.url(_local_path)
return _local_path, url
return None, None
def download_session_replay(session):
replay_storages = ReplayStorage.objects.all()
configs = {
storage.name: storage.config
for storage in replay_storages
if not storage.type_null_or_server
}
if settings.SERVER_REPLAY_STORAGE:
configs['SERVER_REPLAY_STORAGE'] = settings.SERVER_REPLAY_STORAGE
if not configs:
msg = "Not found replay file, and not remote storage set"
return None, msg
storage = jms_storage.get_multi_object_storage(configs)
# 获取外部存储路径名
session_path = session.find_ok_relative_path_in_storage(storage)
if not session_path:
msg = "Not found session replay file"
return None, msg
# 通过外部存储路径名后缀,构造真实的本地存储路径
local_path = session.get_local_path_by_relative_path(session_path)
# 保存到storage的路径
target_path = os.path.join(default_storage.base_location, local_path)
target_dir = os.path.dirname(target_path)
if not os.path.isdir(target_dir):
make_dirs(target_dir, exist_ok=True)
ok, err = storage.download(session_path, target_path)
if not ok:
msg = "Failed download replay file: {}".format(err)
logger.error(msg)
return None, msg
url = default_storage.url(local_path)
return local_path, url
def get_session_replay_url(session):
local_path, url = find_session_replay_local(session)
if local_path is None:
local_path, url = download_session_replay(session)
return local_path, url