1# Copyright (c) 2012-2016 Seafile Ltd.
2import os
3import stat
4import logging
5import posixpath
6
7from rest_framework.authentication import SessionAuthentication
8from rest_framework.permissions import IsAuthenticated
9from rest_framework.response import Response
10from rest_framework.views import APIView
11from rest_framework import status
12from django.utils.http import urlquote
13
14from seahub.api2.throttling import UserRateThrottle
15from seahub.api2.authentication import TokenAuthentication
16from seahub.api2.utils import api_error, to_python_boolean
17from seahub.api2.views import get_dir_file_recursively
18
19from seahub.thumbnail.utils import get_thumbnail_src
20from seahub.views import check_folder_permission
21from seahub.utils import check_filename_with_rename, is_valid_dirent_name, \
22        normalize_dir_path, is_pro_version, FILEEXT_TYPE_MAP
23from seahub.utils.timeutils import timestamp_to_isoformat_timestr
24from seahub.utils.file_tags import get_files_tags_in_dir
25from seahub.utils.file_types import IMAGE, VIDEO, XMIND
26from seahub.base.models import UserStarredFiles
27from seahub.base.templatetags.seahub_tags import email2nickname, \
28        email2contact_email
29
30from seahub.settings import ENABLE_VIDEO_THUMBNAIL, \
31        THUMBNAIL_ROOT
32
33from seaserv import seafile_api
34from pysearpc import SearpcError
35
36logger = logging.getLogger(__name__)
37
38
39def get_dir_file_info_list(username, request_type, repo_obj, parent_dir,
40        with_thumbnail, thumbnail_size):
41
42    repo_id = repo_obj.id
43    dir_info_list = []
44    file_info_list = []
45
46    # get dirent(folder and file) list
47    parent_dir_id = seafile_api.get_dir_id_by_path(repo_id, parent_dir)
48    dir_file_list = seafile_api.list_dir_with_perm(repo_id,
49            parent_dir, parent_dir_id, username, -1, -1)
50
51    try:
52        starred_items = UserStarredFiles.objects.filter(email=username,
53                repo_id=repo_id, path__startswith=parent_dir, org_id=-1)
54        starred_item_path_list = [f.path.rstrip('/') for f in starred_items]
55    except Exception as e:
56        logger.error(e)
57        starred_item_path_list = []
58
59    # only get dir info list
60    if not request_type or request_type == 'd':
61        dir_list = [dirent for dirent in dir_file_list if stat.S_ISDIR(dirent.mode)]
62        for dirent in dir_list:
63            dir_info = {}
64            dir_info["type"] = "dir"
65            dir_info["id"] = dirent.obj_id
66            dir_info["name"] = dirent.obj_name
67            dir_info["mtime"] = dirent.mtime
68            dir_info["permission"] = dirent.permission
69            dir_info["parent_dir"] = parent_dir
70            dir_info_list.append(dir_info)
71
72            # get star info
73            dir_info['starred'] = False
74            dir_path = posixpath.join(parent_dir, dirent.obj_name)
75            if dir_path.rstrip('/') in starred_item_path_list:
76                dir_info['starred'] = True
77
78    # only get file info list
79    if not request_type or request_type == 'f':
80
81        file_list = [dirent for dirent in dir_file_list if not stat.S_ISDIR(dirent.mode)]
82
83        # Use dict to reduce memcache fetch cost in large for-loop.
84        nickname_dict = {}
85        contact_email_dict = {}
86        modifier_set = {x.modifier for x in file_list}
87        lock_owner_set = {x.lock_owner for x in file_list}
88        for e in modifier_set | lock_owner_set:
89            if e not in nickname_dict:
90                nickname_dict[e] = email2nickname(e)
91            if e not in contact_email_dict:
92                contact_email_dict[e] = email2contact_email(e)
93
94        try:
95            files_tags_in_dir = get_files_tags_in_dir(repo_id, parent_dir)
96        except Exception as e:
97            logger.error(e)
98            files_tags_in_dir = {}
99
100        for dirent in file_list:
101
102            file_name = dirent.obj_name
103            file_path = posixpath.join(parent_dir, file_name)
104            file_obj_id = dirent.obj_id
105
106            file_info = {}
107            file_info["type"] = "file"
108            file_info["id"] = file_obj_id
109            file_info["name"] = file_name
110            file_info["mtime"] = dirent.mtime
111            file_info["permission"] = dirent.permission
112            file_info["parent_dir"] = parent_dir
113            file_info["size"] = dirent.size
114
115            modifier_email = dirent.modifier
116            file_info['modifier_email'] = modifier_email
117            file_info['modifier_name'] = nickname_dict.get(modifier_email, '')
118            file_info['modifier_contact_email'] = contact_email_dict.get(modifier_email, '')
119
120            # get lock info
121            if is_pro_version():
122                file_info["is_locked"] = dirent.is_locked
123                file_info["lock_time"] = dirent.lock_time
124
125                lock_owner_email = dirent.lock_owner or ''
126                file_info["lock_owner"] = lock_owner_email
127                file_info['lock_owner_name'] = nickname_dict.get(lock_owner_email, '')
128                file_info['lock_owner_contact_email'] = contact_email_dict.get(lock_owner_email, '')
129
130                if username == lock_owner_email:
131                    file_info["locked_by_me"] = True
132                else:
133                    file_info["locked_by_me"] = False
134
135            # get star info
136            file_info['starred'] = False
137            if file_path.rstrip('/') in starred_item_path_list:
138                file_info['starred'] = True
139
140            # get tag info
141            file_tags = files_tags_in_dir.get(file_name, [])
142            if file_tags:
143                file_info['file_tags'] = []
144                for file_tag in file_tags:
145                    file_info['file_tags'].append(file_tag)
146
147            # get thumbnail info
148            if with_thumbnail and not repo_obj.encrypted:
149
150                # used for providing a way to determine
151                # if send a request to create thumbnail.
152
153                fileExt = os.path.splitext(file_name)[1][1:].lower()
154                file_type = FILEEXT_TYPE_MAP.get(fileExt)
155
156                if file_type in (IMAGE, XMIND) or \
157                        file_type == VIDEO and ENABLE_VIDEO_THUMBNAIL:
158
159                    # if thumbnail has already been created, return its src.
160                    # Then web browser will use this src to get thumbnail instead of
161                    # recreating it.
162                    thumbnail_file_path = os.path.join(THUMBNAIL_ROOT,
163                            str(thumbnail_size), file_obj_id)
164                    if os.path.exists(thumbnail_file_path):
165                        src = get_thumbnail_src(repo_id, thumbnail_size, file_path)
166                        file_info['encoded_thumbnail_src'] = urlquote(src)
167
168            file_info_list.append(file_info)
169
170    dir_info_list.sort(key=lambda x: x['name'].lower())
171    file_info_list.sort(key=lambda x: x['name'].lower())
172
173    return dir_info_list, file_info_list
174
175
176class DirView(APIView):
177    """
178    Support uniform interface for directory operations, including
179    create/delete/rename/list, etc.
180    """
181    authentication_classes = (TokenAuthentication, SessionAuthentication)
182    permission_classes = (IsAuthenticated, )
183    throttle_classes = (UserRateThrottle, )
184
185    def get_dir_info(self, repo_id, dir_path):
186
187        dir_obj = seafile_api.get_dirent_by_path(repo_id, dir_path)
188        dir_info = {
189            'type': 'dir',
190            'repo_id': repo_id,
191            'parent_dir': os.path.dirname(dir_path.rstrip('/')),
192            'obj_name': dir_obj.obj_name if dir_obj else '',
193            'obj_id': dir_obj.obj_id if dir_obj else '',
194            'mtime': timestamp_to_isoformat_timestr(dir_obj.mtime) if dir_obj else '',
195        }
196
197        return dir_info
198
199    def get(self, request, repo_id, format=None):
200        """ Get sub dirent list info.
201
202        Permission checking:
203        1. user with either 'r' or 'rw' permission.
204        """
205
206        # argument check
207        recursive = request.GET.get('recursive', '0')
208        if recursive not in ('1', '0'):
209            error_msg = "If you want to get recursive dir entries, you should set 'recursive' argument as '1'."
210            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
211
212        request_type = request.GET.get('t', '')
213        if request_type and request_type not in ('f', 'd'):
214            error_msg = "'t'(type) should be 'f' or 'd'."
215            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
216
217        with_thumbnail = request.GET.get('with_thumbnail', 'false')
218        if with_thumbnail not in ('true', 'false'):
219            error_msg = 'with_thumbnail invalid.'
220            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
221
222        with_thumbnail = to_python_boolean(with_thumbnail)
223        thumbnail_size = request.GET.get('thumbnail_size', 48)
224        try:
225            thumbnail_size = int(thumbnail_size)
226        except ValueError:
227            error_msg = 'thumbnail_size invalid.'
228            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
229
230        with_parents = request.GET.get('with_parents', 'false')
231        if with_parents not in ('true', 'false'):
232            error_msg = 'with_parents invalid.'
233            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
234
235        with_parents = to_python_boolean(with_parents)
236
237        # recource check
238        repo = seafile_api.get_repo(repo_id)
239        if not repo:
240            error_msg = 'Library %s not found.' % repo_id
241            return api_error(status.HTTP_404_NOT_FOUND, error_msg)
242
243        parent_dir = request.GET.get('p', '/')
244        parent_dir = normalize_dir_path(parent_dir)
245
246        dir_id = seafile_api.get_dir_id_by_path(repo_id, parent_dir)
247        if not dir_id:
248            error_msg = 'Folder %s not found.' % parent_dir
249            return api_error(status.HTTP_404_NOT_FOUND, error_msg)
250
251        # permission check
252        permission = check_folder_permission(request, repo_id, parent_dir)
253        if not permission:
254            error_msg = 'Permission denied.'
255            return api_error(status.HTTP_403_FORBIDDEN, error_msg)
256
257        # get dir/file list recursively
258        username = request.user.username
259        if recursive == '1':
260
261            try:
262                dir_file_info_list = get_dir_file_recursively(username, repo_id,
263                        parent_dir, [])
264            except Exception as e:
265                logger.error(e)
266                error_msg = 'Internal Server Error'
267                return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
268
269            response_dict = {}
270            response_dict['dirent_list'] = []
271
272            if request_type == 'f':
273                for item in dir_file_info_list:
274                    if item['type'] == 'file':
275                        response_dict['dirent_list'].append(item)
276            elif request_type == 'd':
277                for item in dir_file_info_list:
278                    if item['type'] == 'dir':
279                        response_dict['dirent_list'].append(item)
280            else:
281                response_dict['dirent_list'] = dir_file_info_list
282
283            return Response(response_dict)
284
285        parent_dir_list = []
286        if not with_parents:
287            # only return dirent list in current parent folder
288            parent_dir_list.append(parent_dir)
289        else:
290            # if value of 'p' parameter is '/a/b/c' add with_parents's is 'true'
291            # then return dirent list in '/', '/a', '/a/b' and '/a/b/c'.
292            if parent_dir == '/':
293                parent_dir_list.append(parent_dir)
294            else:
295                tmp_parent_dir = '/'
296                parent_dir_list.append(tmp_parent_dir)
297                for folder_name in parent_dir.strip('/').split('/'):
298                    tmp_parent_dir = posixpath.join(tmp_parent_dir, folder_name)
299                    tmp_parent_dir = normalize_dir_path(tmp_parent_dir)
300                    parent_dir_list.append(tmp_parent_dir)
301
302        all_dir_info_list = []
303        all_file_info_list = []
304
305        try:
306            for parent_dir in parent_dir_list:
307                # get dir file info list
308                dir_info_list, file_info_list = get_dir_file_info_list(username,
309                        request_type, repo, parent_dir, with_thumbnail, thumbnail_size)
310                all_dir_info_list.extend(dir_info_list)
311                all_file_info_list.extend(file_info_list)
312        except Exception as e:
313            logger.error(e)
314            error_msg = 'Internal Server Error'
315            return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
316
317        response_dict = {}
318        response_dict["user_perm"] = permission
319        response_dict["dir_id"] = dir_id
320
321        if request_type == 'f':
322            response_dict['dirent_list'] = all_file_info_list
323        elif request_type == 'd':
324            response_dict['dirent_list'] = all_dir_info_list
325        else:
326            response_dict['dirent_list'] = all_dir_info_list + all_file_info_list
327
328        return Response(response_dict)
329
330    def post(self, request, repo_id, format=None):
331        """ Create, rename, revert dir.
332
333        Permission checking:
334        1. create: user with 'rw' permission for current dir's parent dir;
335        2. rename: user with 'rw' permission for current dir;
336        3. revert: user with 'rw' permission for current dir's parent dir;
337        """
338
339        # argument check
340        path = request.GET.get('p', None)
341        if not path or path[0] != '/':
342            error_msg = 'p invalid.'
343            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
344
345        if path == '/':
346            error_msg = 'Can not operate root dir.'
347            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
348
349        operation = request.data.get('operation', None)
350        if not operation:
351            error_msg = 'operation invalid.'
352            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
353
354        operation = operation.lower()
355        if operation not in ('mkdir', 'rename', 'revert'):
356            error_msg = "operation can only be 'mkdir', 'rename' or 'revert'."
357            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
358
359        # resource check
360        repo = seafile_api.get_repo(repo_id)
361        if not repo:
362            error_msg = 'Library %s not found.' % repo_id
363            return api_error(status.HTTP_404_NOT_FOUND, error_msg)
364
365        path = path.rstrip('/')
366        username = request.user.username
367        parent_dir = os.path.dirname(path)
368        if operation == 'mkdir':
369            # resource check
370            parent_dir_id = seafile_api.get_dir_id_by_path(repo_id, parent_dir)
371            if not parent_dir_id:
372                error_msg = 'Folder %s not found.' % parent_dir
373                return api_error(status.HTTP_404_NOT_FOUND, error_msg)
374
375            # permission check
376            if check_folder_permission(request, repo_id, parent_dir) != 'rw':
377                error_msg = 'Permission denied.'
378                return api_error(status.HTTP_403_FORBIDDEN, error_msg)
379
380            new_dir_name = os.path.basename(path)
381
382            if not is_valid_dirent_name(new_dir_name):
383                return api_error(status.HTTP_400_BAD_REQUEST,
384                                 'name invalid.')
385
386            retry_count = 0
387            while retry_count < 10:
388                new_dir_name = check_filename_with_rename(repo_id,
389                        parent_dir, new_dir_name)
390                try:
391                    seafile_api.post_dir(repo_id,
392                            parent_dir, new_dir_name, username)
393                    break
394                except SearpcError as e:
395                    if str(e) == 'file already exists':
396                        retry_count += 1
397                    else:
398                        logger.error(e)
399                        error_msg = 'Internal Server Error'
400                        return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR,
401                                error_msg)
402
403            new_dir_path = posixpath.join(parent_dir, new_dir_name)
404            dir_info = self.get_dir_info(repo_id, new_dir_path)
405            resp = Response(dir_info)
406
407            return resp
408
409        if operation == 'rename':
410            # resource check
411            dir_id = seafile_api.get_dir_id_by_path(repo_id, path)
412            if not dir_id:
413                error_msg = 'Folder %s not found.' % path
414                return api_error(status.HTTP_404_NOT_FOUND, error_msg)
415
416            # permission check
417            if check_folder_permission(request, repo_id, path) != 'rw':
418                error_msg = 'Permission denied.'
419                return api_error(status.HTTP_403_FORBIDDEN, error_msg)
420
421            old_dir_name = os.path.basename(path)
422            new_dir_name = request.data.get('newname', None)
423
424            if not new_dir_name:
425                error_msg = 'newname invalid.'
426                return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
427
428            if not is_valid_dirent_name(new_dir_name):
429                return api_error(status.HTTP_400_BAD_REQUEST,
430                                 'name invalid.')
431
432            if new_dir_name == old_dir_name:
433                dir_info = self.get_dir_info(repo_id, path)
434                resp = Response(dir_info)
435                return resp
436
437            try:
438                # rename duplicate name
439                new_dir_name = check_filename_with_rename(repo_id, parent_dir, new_dir_name)
440                # rename dir
441                seafile_api.rename_file(repo_id, parent_dir, old_dir_name,
442                                        new_dir_name, username)
443
444                new_dir_path = posixpath.join(parent_dir, new_dir_name)
445                dir_info = self.get_dir_info(repo_id, new_dir_path)
446                resp = Response(dir_info)
447                return resp
448            except SearpcError as e:
449                logger.error(e)
450                error_msg = 'Internal Server Error'
451                return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
452
453        if operation == 'revert':
454            commit_id = request.data.get('commit_id', None)
455            if not commit_id:
456                error_msg = 'commit_id invalid.'
457                return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
458
459            if seafile_api.get_dir_id_by_path(repo_id, path):
460                # dir exists in repo
461                if check_folder_permission(request, repo_id, path) != 'rw':
462                    error_msg = 'Permission denied.'
463                    return api_error(status.HTTP_403_FORBIDDEN, error_msg)
464            else:
465                # dir NOT exists in repo
466                if check_folder_permission(request, repo_id, '/') != 'rw':
467                    error_msg = 'Permission denied.'
468                    return api_error(status.HTTP_403_FORBIDDEN, error_msg)
469
470            try:
471                seafile_api.revert_dir(repo_id, commit_id, path, username)
472            except Exception as e:
473                logger.error(e)
474                error_msg = 'Internal Server Error'
475                return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
476
477            return Response({'success': True})
478
479    def delete(self, request, repo_id, format=None):
480        """ Delete dir.
481
482        Permission checking:
483        1. user with 'rw' permission.
484        """
485
486        # argument check
487        path = request.GET.get('p', None)
488        if not path:
489            error_msg = 'p invalid.'
490            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
491
492        if path == '/':
493            error_msg = 'Can not delete root path.'
494            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
495
496        # resource check
497        dir_id = seafile_api.get_dir_id_by_path(repo_id, path)
498        if not dir_id:
499            error_msg = 'Folder %s not found.' % path
500            return api_error(status.HTTP_404_NOT_FOUND, error_msg)
501
502        repo = seafile_api.get_repo(repo_id)
503        if not repo:
504            error_msg = 'Library %s not found.' % repo_id
505            return api_error(status.HTTP_404_NOT_FOUND, error_msg)
506
507        # permission check
508        if check_folder_permission(request, repo_id, path) != 'rw':
509            error_msg = 'Permission denied.'
510            return api_error(status.HTTP_403_FORBIDDEN, error_msg)
511
512        if path[-1] == '/':
513            path = path[:-1]
514
515        path = path.rstrip('/')
516        username = request.user.username
517        parent_dir = os.path.dirname(path)
518        dir_name = os.path.basename(path)
519        try:
520            seafile_api.del_file(repo_id, parent_dir, dir_name, username)
521        except SearpcError as e:
522            logger.error(e)
523            error_msg = 'Internal Server Error'
524            return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
525
526        return Response({'success': True})
527
528
529class DirDetailView(APIView):
530    """ Get detailed info of a folder.
531    """
532    authentication_classes = (TokenAuthentication, SessionAuthentication)
533    permission_classes = (IsAuthenticated, )
534    throttle_classes = (UserRateThrottle, )
535
536    def get(self, request, repo_id):
537        """ Get dir info.
538
539        Permission checking:
540        1. user with either 'r' or 'rw' permission.
541        """
542
543        # parameter check
544        path = request.GET.get('path', None)
545        if not path:
546            error_msg = 'path invalid.'
547            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
548
549        path = normalize_dir_path(path)
550        if path == '/':
551            error_msg = 'path invalid.'
552            return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
553
554        # resource check
555        repo = seafile_api.get_repo(repo_id)
556        if not repo:
557            error_msg = 'Library %s not found.' % repo_id
558            return api_error(status.HTTP_404_NOT_FOUND, error_msg)
559
560        dir_id = seafile_api.get_dir_id_by_path(repo_id, path)
561        if not dir_id:
562            error_msg = 'Folder %s not found.' % path
563            return api_error(status.HTTP_404_NOT_FOUND, error_msg)
564
565        # permission check
566        permission = check_folder_permission(request, repo_id, path)
567        if not permission:
568            error_msg = 'Permission denied.'
569            return api_error(status.HTTP_403_FORBIDDEN, error_msg)
570
571        dir_obj = seafile_api.get_dirent_by_path(repo_id, path)
572        dir_info = {
573            'repo_id': repo_id,
574            'path': path,
575            'name': dir_obj.obj_name if dir_obj else '',
576            'mtime': timestamp_to_isoformat_timestr(dir_obj.mtime) if dir_obj else '',
577            'permission': permission,
578        }
579
580        return Response(dir_info)
581