1# Copyright (c) 2012-2016 Seafile Ltd.
2import os
3import datetime
4import logging
5from django.db import models
6from django.db.models import Q
7from django.utils import timezone
8
9from pysearpc import SearpcError
10from seaserv import seafile_api
11
12from seahub.auth.signals import user_logged_in
13from seahub.utils import calc_file_path_hash, within_time_range, \
14        normalize_file_path, normalize_dir_path
15from seahub.utils.timeutils import datetime_to_isoformat_timestr
16from seahub.tags.models import FileUUIDMap
17from .fields import LowerCaseCharField
18
19
20# Get an instance of a logger
21logger = logging.getLogger(__name__)
22
23
24class TimestampedModel(models.Model):
25    # A timestamp representing when this object was created.
26    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
27
28    # A timestamp reprensenting when this object was last updated.
29    updated_at = models.DateTimeField(auto_now=True, db_index=True)
30
31    class Meta:
32        abstract = True
33
34        # By default, any model that inherits from `TimestampedModel` should
35        # be ordered in reverse-chronological order. We can override this on a
36        # per-model basis as needed, but reverse-chronological is a good
37        # default ordering for most models.
38        ordering = ['-created_at', '-updated_at']
39
40class FileCommentManager(models.Manager):
41    def add(self, repo_id, parent_path, item_name, author, comment, detail=''):
42        fileuuidmap = FileUUIDMap.objects.get_or_create_fileuuidmap(repo_id,
43                                                                    parent_path,
44                                                                    item_name,
45                                                                    False)
46        c = self.model(uuid=fileuuidmap, author=author, comment=comment, detail=detail)
47        c.save(using=self._db)
48        return c
49
50    def add_by_file_path(self, repo_id, file_path, author, comment, detail=''):
51        file_path = self.model.normalize_path(file_path)
52        parent_path = os.path.dirname(file_path)
53        item_name = os.path.basename(file_path)
54
55        return self.add(repo_id, parent_path, item_name, author, comment, detail)
56
57    def get_by_file_path(self, repo_id, file_path):
58        parent_path = os.path.dirname(file_path)
59        item_name = os.path.basename(file_path)
60        uuid = FileUUIDMap.objects.get_fileuuidmap_by_path(repo_id, parent_path,
61                                                           item_name, False)
62
63        objs = super(FileCommentManager, self).filter(
64            uuid=uuid)
65
66        return objs
67
68    def get_by_parent_path(self, repo_id, parent_path):
69        uuids = FileUUIDMap.objects.get_fileuuidmaps_by_parent_path(repo_id,
70                                                                   parent_path)
71        objs = super(FileCommentManager, self).filter(uuid__in=uuids)
72        return objs
73
74
75class FileComment(models.Model):
76    """
77    Model used to record file comments.
78    """
79    uuid = models.ForeignKey(FileUUIDMap, on_delete=models.CASCADE)
80    author = LowerCaseCharField(max_length=255, db_index=True)
81    comment = models.TextField()
82    created_at = models.DateTimeField(default=timezone.now)
83    updated_at = models.DateTimeField(default=timezone.now)
84    resolved = models.BooleanField(default=False, db_index=True)
85    detail = models.TextField(default='')
86
87    objects = FileCommentManager()
88
89    @classmethod
90    def normalize_path(self, path):
91        return path.rstrip('/') if path != '/' else '/'
92
93    def to_dict(self):
94        o = self
95        return {
96            'id': o.pk,
97            'repo_id': o.uuid.repo_id,
98            'parent_path': o.uuid.parent_path,
99            'item_name': o.uuid.filename,
100            'comment': o.comment,
101            'created_at': datetime_to_isoformat_timestr(o.created_at),
102            'resolved': o.resolved,
103            'detail': o.detail,
104        }
105
106
107########## starred files
108class StarredFile(object):
109    def format_path(self):
110        if self.path == "/":
111            return self.path
112
113        # strip leading slash
114        path = self.path[1:]
115        if path[-1:] == '/':
116            path = path[:-1]
117        return path.replace('/', ' / ')
118
119    def __init__(self, org_id, repo, file_id, path, is_dir, size):
120        # always 0 for non-org repo
121        self.org_id = org_id
122        self.repo = repo
123        self.file_id = file_id
124        self.path = path
125        self.formatted_path = self.format_path()
126        self.is_dir = is_dir
127        self.size = size
128        self.last_modified = None
129        if not is_dir:
130            self.name = path.split('/')[-1]
131
132class UserStarredFilesManager(models.Manager):
133
134    def get_starred_repos_by_user(self, email):
135
136        starred_repos = UserStarredFiles.objects.filter(email=email, path='/')
137        return starred_repos
138
139    def get_starred_item(self, email, repo_id, path):
140
141        path_list = [normalize_file_path(path), normalize_dir_path(path)]
142        starred_items = UserStarredFiles.objects.filter(email=email,
143                repo_id=repo_id).filter(Q(path__in=path_list))
144
145        return starred_items[0] if len(starred_items) > 0 else None
146
147    def add_starred_item(self, email, repo_id, path, is_dir, org_id=-1):
148
149        starred_item = UserStarredFiles.objects.create(email=email,
150                repo_id=repo_id, path=path, is_dir=is_dir, org_id=org_id)
151
152        return starred_item
153
154    def delete_starred_item(self, email, repo_id, path):
155
156        path_list = [normalize_file_path(path), normalize_dir_path(path)]
157        starred_items = UserStarredFiles.objects.filter(email=email,
158                repo_id=repo_id).filter(Q(path__in=path_list))
159
160        for item in starred_items:
161            item.delete()
162
163    def get_starred_files_by_username(self, username):
164        """Get a user's starred files.
165
166        Arguments:
167        - `self`:
168        - `username`:
169        """
170        starred_files = super(UserStarredFilesManager, self).filter(
171            email=username, is_dir=False, org_id=-1)
172
173        ret = []
174        repo_cache = {}
175        for sfile in starred_files:
176            # repo still exists?
177            if sfile.repo_id in repo_cache:
178                repo = repo_cache[sfile.repo_id]
179            else:
180                try:
181                    repo = seafile_api.get_repo(sfile.repo_id)
182                except SearpcError:
183                    continue
184                if repo is not None:
185                    repo_cache[sfile.repo_id] = repo
186                else:
187                    sfile.delete()
188                    continue
189
190            # file still exists?
191            file_id = ''
192            # size = -1
193            if sfile.path != "/":
194                try:
195                    file_id = seafile_api.get_file_id_by_path(sfile.repo_id,
196                                                              sfile.path)
197                    # size = seafile_api.get_file_size(file_id)
198                except SearpcError:
199                    continue
200                if not file_id:
201                    sfile.delete()
202                    continue
203
204            f = StarredFile(sfile.org_id, repo, file_id, sfile.path,
205                            sfile.is_dir, 0) # TODO: remove ``size`` from StarredFile
206            ret.append(f)
207
208        '''Calculate files last modification time'''
209        for sfile in ret:
210            if sfile.is_dir:
211                continue
212
213            try:
214                # get real path for sub repo
215                real_path = sfile.repo.origin_path + sfile.path if sfile.repo.origin_path else sfile.path
216                dirent = seafile_api.get_dirent_by_path(sfile.repo.store_id,
217                                                        real_path)
218                if dirent:
219                    sfile.last_modified = dirent.mtime
220                else:
221                    sfile.last_modified = 0
222            except SearpcError as e:
223                logger.error(e)
224                sfile.last_modified = 0
225
226        ret.sort(key=lambda x: x.last_modified, reverse=True)
227
228        return ret
229
230class UserStarredFiles(models.Model):
231    """Starred files are marked by users to get quick access to it on user
232    home page.
233
234    """
235    email = models.EmailField(db_index=True)
236    org_id = models.IntegerField()
237    repo_id = models.CharField(max_length=36, db_index=True)
238    path = models.TextField()
239    is_dir = models.BooleanField()
240
241    objects = UserStarredFilesManager()
242
243########## misc
244class UserLastLoginManager(models.Manager):
245    def get_by_username(self, username):
246        """Return last login record for a user, delete duplicates if there are
247        duplicated records.
248        """
249        try:
250            return self.get(username=username)
251        except UserLastLogin.DoesNotExist:
252            return None
253        except UserLastLogin.MultipleObjectsReturned:
254            dups = self.filter(username=username)
255            ret = dups[0]
256            for dup in dups[1:]:
257                dup.delete()
258                logger.warn('Delete duplicate user last login record: %s' % username)
259            return ret
260
261class UserLastLogin(models.Model):
262    username = models.CharField(max_length=255, db_index=True)
263    last_login = models.DateTimeField(default=timezone.now)
264    objects = UserLastLoginManager()
265
266def update_last_login(sender, user, **kwargs):
267    """
268    A signal receiver which updates the last_login date for
269    the user logging in.
270    """
271    user_last_login = UserLastLogin.objects.get_by_username(user.username)
272    if user_last_login is None:
273        user_last_login = UserLastLogin(username=user.username)
274    user_last_login.last_login = timezone.now()
275    user_last_login.save()
276user_logged_in.connect(update_last_login)
277
278class CommandsLastCheck(models.Model):
279    """Record last check time for Django/custom commands.
280    """
281    command_type = models.CharField(max_length=100)
282    last_check = models.DateTimeField()
283
284class DeviceToken(models.Model):
285    """
286    The iOS device token model.
287    """
288    token = models.CharField(max_length=80)
289    user = LowerCaseCharField(max_length=255)
290    platform = LowerCaseCharField(max_length=32)
291    version = LowerCaseCharField(max_length=16)
292    pversion = LowerCaseCharField(max_length=16)
293
294    class Meta:
295        unique_together = (("token", "user"),)
296
297    def __unicode__(self):
298        return "/".join(self.user, self.token)
299
300_CLIENT_LOGIN_TOKEN_EXPIRATION_SECONDS = 30
301
302class ClientLoginTokenManager(models.Manager):
303    def get_username(self, tokenstr):
304        try:
305            token = super(ClientLoginTokenManager, self).get(token=tokenstr)
306        except ClientLoginToken.DoesNotExist:
307            return None
308        username = token.username
309        token.delete()
310        if not within_time_range(token.timestamp, timezone.now(),
311                                 _CLIENT_LOGIN_TOKEN_EXPIRATION_SECONDS):
312            return None
313        return username
314
315class ClientLoginToken(models.Model):
316    # TODO: update sql/mysql.sql and sql/sqlite3.sql
317    token = models.CharField(max_length=32, primary_key=True)
318    username = models.CharField(max_length=255, db_index=True)
319    timestamp = models.DateTimeField(default=timezone.now)
320
321    objects = ClientLoginTokenManager()
322
323    def __unicode__(self):
324        return "/".join(self.username, self.token)
325
326
327class RepoSecretKeyManager(models.Manager):
328
329    def get_secret_key(self, repo_id):
330        try:
331            repo_secret_key = self.get(repo_id=repo_id)
332        except RepoSecretKey.DoesNotExist:
333            return None
334
335        return repo_secret_key.secret_key
336
337    def add_secret_key(self, repo_id, secret_key):
338
339        repo_secret_key = self.model(repo_id=repo_id, secret_key=secret_key)
340        repo_secret_key.save(using=self._db)
341
342        return repo_secret_key
343
344
345class RepoSecretKey(models.Model):
346    """
347    """
348    repo_id = models.CharField(unique=True, max_length=36, db_index=True)
349    secret_key = models.CharField(max_length=44)
350
351    objects = RepoSecretKeyManager()
352