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