photo-objects 0.8.6__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- photo_objects/django/admin.py +7 -1
- photo_objects/django/api/__init__.py +1 -0
- photo_objects/django/api/backup.py +162 -0
- photo_objects/django/forms.py +2 -18
- photo_objects/django/management/commands/create-initial-admin-account.py +3 -1
- photo_objects/django/management/commands/restore-backup.py +74 -0
- photo_objects/django/migrations/0007_backup.py +22 -0
- photo_objects/django/models.py +26 -10
- photo_objects/django/objsto.py +108 -37
- photo_objects/django/signals.py +21 -2
- photo_objects/django/tests/utils.py +2 -2
- photo_objects/utils.py +23 -0
- {photo_objects-0.8.6.dist-info → photo_objects-0.9.0.dist-info}/METADATA +1 -1
- {photo_objects-0.8.6.dist-info → photo_objects-0.9.0.dist-info}/RECORD +17 -14
- {photo_objects-0.8.6.dist-info → photo_objects-0.9.0.dist-info}/WHEEL +0 -0
- {photo_objects-0.8.6.dist-info → photo_objects-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {photo_objects-0.8.6.dist-info → photo_objects-0.9.0.dist-info}/top_level.txt +0 -0
photo_objects/django/admin.py
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
from django.contrib import admin
|
|
2
2
|
|
|
3
|
-
from .models import Album, Photo, PhotoChangeRequest, SiteSettings
|
|
3
|
+
from .models import Album, Backup, Photo, PhotoChangeRequest, SiteSettings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BackupAdmin(admin.ModelAdmin):
|
|
7
|
+
readonly_fields = ["status"]
|
|
8
|
+
|
|
4
9
|
|
|
5
10
|
admin.site.register(Album)
|
|
6
11
|
admin.site.register(Photo)
|
|
7
12
|
admin.site.register(SiteSettings)
|
|
8
13
|
admin.site.register(PhotoChangeRequest)
|
|
14
|
+
admin.site.register(Backup, BackupAdmin)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from django.contrib.auth import get_user_model
|
|
2
|
+
from django.contrib.auth.models import Group, Permission
|
|
3
|
+
from django.db import transaction
|
|
4
|
+
|
|
5
|
+
from photo_objects.django.models import Album, Backup, Photo
|
|
6
|
+
from photo_objects.django.objsto import (
|
|
7
|
+
backup_data_key,
|
|
8
|
+
backup_info_key,
|
|
9
|
+
delete_backup_objects,
|
|
10
|
+
get_backup_data,
|
|
11
|
+
get_backup_objects,
|
|
12
|
+
put_backup_json,
|
|
13
|
+
)
|
|
14
|
+
from photo_objects.utils import timestamp_str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _permission_dict(permission: Permission):
|
|
18
|
+
return {
|
|
19
|
+
"app_label": permission.content_type.app_label,
|
|
20
|
+
"codename": permission.codename,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_permissions(permissions=None):
|
|
25
|
+
for permission in (permissions or []):
|
|
26
|
+
yield Permission.objects.get(
|
|
27
|
+
content_type__app_label=permission.get("app_label"),
|
|
28
|
+
codename=permission.get("codename"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _user_dict(user):
|
|
32
|
+
return {
|
|
33
|
+
"username": user.username,
|
|
34
|
+
"first_name": user.first_name,
|
|
35
|
+
"last_name": user.last_name,
|
|
36
|
+
"email": user.email,
|
|
37
|
+
"groups": [
|
|
38
|
+
i.name for i in user.groups.all()],
|
|
39
|
+
"user_permissions": [
|
|
40
|
+
_permission_dict(i) for i in user.user_permissions.all()],
|
|
41
|
+
"is_staff": user.is_staff,
|
|
42
|
+
"is_active": user.is_active,
|
|
43
|
+
"is_superuser": user.is_superuser,
|
|
44
|
+
"last_login": timestamp_str(
|
|
45
|
+
user.last_login),
|
|
46
|
+
"date_joined": timestamp_str(
|
|
47
|
+
user.date_joined)}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _group_dict(group: Group):
|
|
51
|
+
return {
|
|
52
|
+
"name": group.name,
|
|
53
|
+
"permissions": [_permission_dict(i) for i in group.permissions.all()],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_backup(backup: Backup):
|
|
58
|
+
user_model = get_user_model()
|
|
59
|
+
|
|
60
|
+
backup.status = "pending"
|
|
61
|
+
backup.save()
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
albums = Album.objects.all()
|
|
65
|
+
for album in albums:
|
|
66
|
+
album_dict = album.to_json()
|
|
67
|
+
photos = []
|
|
68
|
+
for photo in album.photo_set.all():
|
|
69
|
+
photos.append(photo.to_json())
|
|
70
|
+
album_dict["photos"] = photos
|
|
71
|
+
|
|
72
|
+
put_backup_json(
|
|
73
|
+
backup_data_key(
|
|
74
|
+
backup.id,
|
|
75
|
+
'album',
|
|
76
|
+
album.key),
|
|
77
|
+
album_dict,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
groups = Group.objects.all()
|
|
81
|
+
for group in groups:
|
|
82
|
+
put_backup_json(
|
|
83
|
+
backup_data_key(
|
|
84
|
+
backup.id,
|
|
85
|
+
'group',
|
|
86
|
+
group.name),
|
|
87
|
+
_group_dict(group),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
users = user_model.objects.all()
|
|
91
|
+
for user in users:
|
|
92
|
+
if user.username == "admin":
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
put_backup_json(
|
|
96
|
+
backup_data_key(
|
|
97
|
+
backup.id,
|
|
98
|
+
'user',
|
|
99
|
+
user.username),
|
|
100
|
+
_user_dict(user))
|
|
101
|
+
|
|
102
|
+
put_backup_json(backup_info_key(backup.id), backup.to_json())
|
|
103
|
+
except Exception:
|
|
104
|
+
backup.status = "create_failed"
|
|
105
|
+
backup.save()
|
|
106
|
+
|
|
107
|
+
backup.status = "ready"
|
|
108
|
+
backup.save()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def delete_backup(backup: Backup):
|
|
112
|
+
return delete_backup_objects(backup.id)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_backups() -> list[dict]:
|
|
116
|
+
return get_backup_objects()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@transaction.atomic
|
|
120
|
+
def restore_backup(backup_id: int):
|
|
121
|
+
user_model = get_user_model()
|
|
122
|
+
|
|
123
|
+
for i in get_backups():
|
|
124
|
+
Backup.objects.create(**i)
|
|
125
|
+
|
|
126
|
+
for i in get_backup_data(backup_id, "group"):
|
|
127
|
+
group = Group.objects.create(name=i.get('name'))
|
|
128
|
+
|
|
129
|
+
for permission in _get_permissions(i.get('permissions')):
|
|
130
|
+
group.permissions.add(permission)
|
|
131
|
+
|
|
132
|
+
group.save()
|
|
133
|
+
|
|
134
|
+
for i in get_backup_data(backup_id, "user"):
|
|
135
|
+
permissions = i.pop("user_permissions")
|
|
136
|
+
groups = i.pop("groups")
|
|
137
|
+
|
|
138
|
+
user = user_model.objects.create(**i)
|
|
139
|
+
|
|
140
|
+
for group in groups:
|
|
141
|
+
user.groups.add(Group.objects.get(name=group))
|
|
142
|
+
|
|
143
|
+
for permission in _get_permissions(permissions):
|
|
144
|
+
user.user_permissions.add(permission)
|
|
145
|
+
|
|
146
|
+
user.save()
|
|
147
|
+
|
|
148
|
+
for i in get_backup_data(backup_id, "album"):
|
|
149
|
+
photos = i.pop("photos")
|
|
150
|
+
cover_photo = i.pop("cover_photo")
|
|
151
|
+
|
|
152
|
+
album = Album.objects.create(**i)
|
|
153
|
+
|
|
154
|
+
for photo in photos:
|
|
155
|
+
photo.pop("album", None)
|
|
156
|
+
filename = photo.pop("filename")
|
|
157
|
+
|
|
158
|
+
photo = Photo.objects.create(**photo, album=album)
|
|
159
|
+
|
|
160
|
+
if cover_photo == filename:
|
|
161
|
+
album.cover_photo = photo
|
|
162
|
+
album.save()
|
photo_objects/django/forms.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import random
|
|
2
|
-
import re
|
|
3
|
-
import unicodedata
|
|
4
2
|
|
|
5
3
|
from django import forms
|
|
6
4
|
from django.forms import (
|
|
@@ -16,6 +14,8 @@ from django.forms import (
|
|
|
16
14
|
from django.utils.safestring import mark_safe
|
|
17
15
|
from django.utils.translation import gettext_lazy as _
|
|
18
16
|
|
|
17
|
+
from photo_objects.utils import slugify
|
|
18
|
+
|
|
19
19
|
from .models import Album, Photo, PhotoChangeRequest
|
|
20
20
|
|
|
21
21
|
|
|
@@ -26,22 +26,6 @@ KEY_POSTFIX_LEN = 5
|
|
|
26
26
|
ALT_TEXT_HELP = _('Alternative text content for the photo.')
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def slugify(title: str, lower=False, replace_leading_underscores=False) -> str:
|
|
30
|
-
key = unicodedata.normalize(
|
|
31
|
-
'NFKD', title).encode(
|
|
32
|
-
'ascii', 'ignore').decode('ascii')
|
|
33
|
-
if lower:
|
|
34
|
-
key = key.lower()
|
|
35
|
-
|
|
36
|
-
key = re.sub(r'[^a-zA-Z0-9._-]', '-', key)
|
|
37
|
-
key = re.sub(r'[-_]{2,}', '-', key)
|
|
38
|
-
|
|
39
|
-
if replace_leading_underscores:
|
|
40
|
-
key = re.sub(r'^_+', '-', key)
|
|
41
|
-
|
|
42
|
-
return key
|
|
43
|
-
|
|
44
|
-
|
|
45
29
|
def _postfix_generator():
|
|
46
30
|
for _ in range(13):
|
|
47
31
|
yield '-' + ''.join(
|
|
@@ -12,7 +12,9 @@ class Command(BaseCommand):
|
|
|
12
12
|
|
|
13
13
|
def handle(self, *args, **options):
|
|
14
14
|
user = get_user_model()
|
|
15
|
-
superuser_count = user.objects.filter(
|
|
15
|
+
superuser_count = user.objects.filter(
|
|
16
|
+
is_superuser=True).exclude(
|
|
17
|
+
password="").count()
|
|
16
18
|
|
|
17
19
|
if superuser_count == 0:
|
|
18
20
|
username = 'admin'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# pylint: disable=invalid-name
|
|
2
|
+
from django.core.management.base import BaseCommand
|
|
3
|
+
from django.contrib.auth import get_user_model
|
|
4
|
+
from django.contrib.auth.models import Group
|
|
5
|
+
|
|
6
|
+
from photo_objects.django.api.backup import get_backups, restore_backup
|
|
7
|
+
from photo_objects.django.models import Album, Photo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DatabaseStatus:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
user = get_user_model()
|
|
13
|
+
|
|
14
|
+
self._users = user.objects.count()
|
|
15
|
+
self._groups = Group.objects.count()
|
|
16
|
+
self._albums = Album.objects.count()
|
|
17
|
+
self._photos = Photo.objects.count()
|
|
18
|
+
|
|
19
|
+
def should_restore(self):
|
|
20
|
+
count = self._users + self._groups + self._albums + self._photos
|
|
21
|
+
return count == 0
|
|
22
|
+
|
|
23
|
+
def __str__(self):
|
|
24
|
+
return (
|
|
25
|
+
f"Database contains {self._users} users, "
|
|
26
|
+
f"{self._groups} groups, {self._albums} albums, and "
|
|
27
|
+
f"{self._photos} photos"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Command(BaseCommand):
|
|
32
|
+
help = "Restore latest backup if database is empty."
|
|
33
|
+
|
|
34
|
+
def handle(self, *args, **options):
|
|
35
|
+
status = DatabaseStatus()
|
|
36
|
+
if not status.should_restore():
|
|
37
|
+
self.stdout.write(
|
|
38
|
+
self.style.NOTICE(
|
|
39
|
+
f'Restoring backup skipped: {status}'
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
backups = get_backups()
|
|
45
|
+
|
|
46
|
+
if not backups:
|
|
47
|
+
self.stdout.write(
|
|
48
|
+
self.style.NOTICE(
|
|
49
|
+
'Restoring backup skipped: No backups found.'
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
id_ = backups[-1].get("id")
|
|
56
|
+
self.stdout.write(
|
|
57
|
+
self.style.NOTICE(
|
|
58
|
+
f'Restoring backup with ID {id_}.'
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
restore_backup(id_)
|
|
62
|
+
status = DatabaseStatus()
|
|
63
|
+
self.stdout.write(
|
|
64
|
+
self.style.SUCCESS(
|
|
65
|
+
f'Restored backup with ID {id_}: {status}'
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
self.stdout.write(
|
|
70
|
+
self.style.ERROR(
|
|
71
|
+
f'Failed to restore backup: {e}'
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
exit(1)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Generated by Django 5.2.6 on 2025-10-26 23:26
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('photo_objects', '0006_photo_alt_text'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.CreateModel(
|
|
14
|
+
name='Backup',
|
|
15
|
+
fields=[
|
|
16
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
17
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
18
|
+
('comment', models.TextField(blank=True)),
|
|
19
|
+
('status', models.TextField(blank=True)),
|
|
20
|
+
],
|
|
21
|
+
),
|
|
22
|
+
]
|
photo_objects/django/models.py
CHANGED
|
@@ -4,7 +4,7 @@ from django.contrib.sites.models import Site
|
|
|
4
4
|
from django.core.validators import RegexValidator
|
|
5
5
|
from django.utils.translation import gettext_lazy as _
|
|
6
6
|
|
|
7
|
-
from photo_objects.utils import first_paragraph_textcontent
|
|
7
|
+
from photo_objects.utils import first_paragraph_textcontent, timestamp_str
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
album_key_validator = RegexValidator(
|
|
@@ -23,10 +23,6 @@ def _str(key, **kwargs):
|
|
|
23
23
|
return f'{key} ({details})' if details else key
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def _timestamp_str(timestamp):
|
|
27
|
-
return timestamp.isoformat() if timestamp else None
|
|
28
|
-
|
|
29
|
-
|
|
30
26
|
class BaseModel(models.Model):
|
|
31
27
|
title = models.CharField(blank=True)
|
|
32
28
|
description = models.TextField(blank=True)
|
|
@@ -41,8 +37,8 @@ class BaseModel(models.Model):
|
|
|
41
37
|
return dict(
|
|
42
38
|
title=self.title,
|
|
43
39
|
description=self.description,
|
|
44
|
-
created_at=
|
|
45
|
-
updated_at=
|
|
40
|
+
created_at=timestamp_str(self.created_at),
|
|
41
|
+
updated_at=timestamp_str(self.updated_at),
|
|
46
42
|
)
|
|
47
43
|
|
|
48
44
|
|
|
@@ -82,8 +78,8 @@ class Album(BaseModel):
|
|
|
82
78
|
visibility=self.visibility,
|
|
83
79
|
cover_photo=(
|
|
84
80
|
self.cover_photo.filename if self.cover_photo else None),
|
|
85
|
-
first_timestamp=
|
|
86
|
-
last_timestamp=
|
|
81
|
+
first_timestamp=timestamp_str(self.first_timestamp),
|
|
82
|
+
last_timestamp=timestamp_str(self.last_timestamp),
|
|
87
83
|
)
|
|
88
84
|
|
|
89
85
|
|
|
@@ -190,7 +186,7 @@ class PhotoChangeRequest(models.Model):
|
|
|
190
186
|
return dict(
|
|
191
187
|
id=self.id,
|
|
192
188
|
photo=self.photo.key,
|
|
193
|
-
created_at=
|
|
189
|
+
created_at=timestamp_str(self.created_at),
|
|
194
190
|
alt_text=self.alt_text,
|
|
195
191
|
)
|
|
196
192
|
|
|
@@ -233,3 +229,23 @@ def clear_cached_settings(sender, **kwargs):
|
|
|
233
229
|
|
|
234
230
|
pre_save.connect(clear_cached_settings, sender=SiteSettings)
|
|
235
231
|
pre_delete.connect(clear_cached_settings, sender=SiteSettings)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class Backup(models.Model):
|
|
235
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
236
|
+
comment = models.TextField(blank=True)
|
|
237
|
+
status = models.TextField(blank=True)
|
|
238
|
+
|
|
239
|
+
def __str__(self):
|
|
240
|
+
return _str(
|
|
241
|
+
f'Backup {self.id}',
|
|
242
|
+
created_at=self.created_at,
|
|
243
|
+
comment=self.comment,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def to_json(self):
|
|
247
|
+
return dict(
|
|
248
|
+
id=self.id,
|
|
249
|
+
created_at=timestamp_str(self.created_at),
|
|
250
|
+
comment=self.comment,
|
|
251
|
+
)
|
photo_objects/django/objsto.py
CHANGED
|
@@ -13,6 +13,7 @@ from photo_objects.django.conf import (
|
|
|
13
13
|
objsto_settings,
|
|
14
14
|
parse_photo_sizes,
|
|
15
15
|
)
|
|
16
|
+
from photo_objects.utils import slugify
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
MEGABYTE = 1 << 20
|
|
@@ -33,20 +34,35 @@ def _anonymous_readonly_policy(bucket: str):
|
|
|
33
34
|
return json.dumps(policy)
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
def _objsto_access() -> tuple[
|
|
37
|
+
def _objsto_access() -> tuple[dict, Minio]:
|
|
37
38
|
conf = objsto_settings()
|
|
38
39
|
http = urllib3.PoolManager(
|
|
39
40
|
retries=urllib3.util.Retry(connect=1),
|
|
40
41
|
timeout=urllib3.util.Timeout(connect=2.5, read=20),
|
|
41
42
|
)
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
return (conf, Minio(
|
|
44
45
|
conf.get('URL'),
|
|
45
46
|
conf.get('ACCESS_KEY'),
|
|
46
47
|
conf.get('SECRET_KEY'),
|
|
47
48
|
http_client=http,
|
|
48
49
|
secure=conf.get('SECURE', True),
|
|
49
|
-
)
|
|
50
|
+
))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _backup_access() -> tuple[Minio, str]:
|
|
54
|
+
conf, client = _objsto_access()
|
|
55
|
+
bucket = conf.get('BACKUP_BUCKET', 'backups')
|
|
56
|
+
|
|
57
|
+
# TODO: move this to management command
|
|
58
|
+
if not client.bucket_exists(bucket):
|
|
59
|
+
client.make_bucket(bucket)
|
|
60
|
+
|
|
61
|
+
return client, bucket
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _photos_access() -> tuple[Minio, str]:
|
|
65
|
+
conf, client = _objsto_access()
|
|
50
66
|
bucket = conf.get('BUCKET', 'photos')
|
|
51
67
|
|
|
52
68
|
# TODO: move this to management command
|
|
@@ -57,6 +73,88 @@ def _objsto_access() -> tuple[Minio, str]:
|
|
|
57
73
|
return client, bucket
|
|
58
74
|
|
|
59
75
|
|
|
76
|
+
def _put_json(key, data, access_fn):
|
|
77
|
+
data_str = json.dumps(data)
|
|
78
|
+
stream = BytesIO(data_str.encode('utf-8'))
|
|
79
|
+
|
|
80
|
+
client, bucket = access_fn()
|
|
81
|
+
client.put_object(
|
|
82
|
+
bucket,
|
|
83
|
+
key,
|
|
84
|
+
stream,
|
|
85
|
+
length=-1,
|
|
86
|
+
part_size=10 * MEGABYTE,
|
|
87
|
+
content_type="application/json",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _list_all(client: Minio, bucket: str, prefix: str):
|
|
92
|
+
start_after = None
|
|
93
|
+
while True:
|
|
94
|
+
objects = client.list_objects(
|
|
95
|
+
bucket,
|
|
96
|
+
prefix=prefix,
|
|
97
|
+
recursive=True,
|
|
98
|
+
start_after=start_after)
|
|
99
|
+
|
|
100
|
+
if not objects:
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
empty = True
|
|
104
|
+
for i in objects:
|
|
105
|
+
empty = False
|
|
106
|
+
yield i
|
|
107
|
+
start_after = i.object_name
|
|
108
|
+
|
|
109
|
+
if empty:
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _get_all(client: Minio, bucket: str, prefix: str):
|
|
114
|
+
for i in _list_all(client, bucket, prefix):
|
|
115
|
+
yield client.get_object(bucket, i.object_name)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _delete_all(client: Minio, bucket: str, prefix: str):
|
|
119
|
+
for i in _list_all(client, bucket, prefix):
|
|
120
|
+
client.remove_object(bucket, i.object_name)
|
|
121
|
+
yield i.object_name
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def backup_info_key(id_):
|
|
125
|
+
return f'info_{id_}.json'
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def backup_data_key(id_, type_, key):
|
|
129
|
+
return f'data_{id_}/{type_}_{slugify(key)}.json'
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def backup_data_prefix(id_, type_=None):
|
|
133
|
+
return f'data_{id_}/{type_ or ""}'
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def put_backup_json(key: str, data: dict):
|
|
137
|
+
return _put_json(key, data, _backup_access)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_backup_objects():
|
|
141
|
+
client, bucket = _backup_access()
|
|
142
|
+
return [json.loads(i.read()) for i in _get_all(client, bucket, 'info_')]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_backup_data(id_: int, type_=None):
|
|
146
|
+
client, bucket = _backup_access()
|
|
147
|
+
for i in _get_all(client, bucket, backup_data_prefix(id_, type_)):
|
|
148
|
+
yield json.loads(i.read())
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def delete_backup_objects(id_: int):
|
|
152
|
+
client, bucket = _backup_access()
|
|
153
|
+
client.remove_object(bucket, backup_info_key(id_))
|
|
154
|
+
for _ in _delete_all(client, bucket, backup_data_prefix(id_)):
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
|
|
60
158
|
def photo_path(album_key, photo_key, size_key):
|
|
61
159
|
return f"{size_key}/{album_key}/{photo_key}"
|
|
62
160
|
|
|
@@ -86,7 +184,7 @@ def photo_content_headers(
|
|
|
86
184
|
def put_photo(album_key, photo_key, size_key, photo_file, image_format=None):
|
|
87
185
|
content_type, headers = photo_content_headers(photo_key, image_format)
|
|
88
186
|
|
|
89
|
-
client, bucket =
|
|
187
|
+
client, bucket = _photos_access()
|
|
90
188
|
return client.put_object(
|
|
91
189
|
bucket,
|
|
92
190
|
photo_path(album_key, photo_key, size_key),
|
|
@@ -99,7 +197,7 @@ def put_photo(album_key, photo_key, size_key, photo_file, image_format=None):
|
|
|
99
197
|
|
|
100
198
|
|
|
101
199
|
def get_photo(album_key, photo_key, size_key):
|
|
102
|
-
client, bucket =
|
|
200
|
+
client, bucket = _photos_access()
|
|
103
201
|
return client.get_object(
|
|
104
202
|
bucket,
|
|
105
203
|
photo_path(album_key, photo_key, size_key)
|
|
@@ -107,33 +205,17 @@ def get_photo(album_key, photo_key, size_key):
|
|
|
107
205
|
|
|
108
206
|
|
|
109
207
|
def delete_photo(album_key, photo_key):
|
|
110
|
-
client, bucket =
|
|
208
|
+
client, bucket = _photos_access()
|
|
111
209
|
|
|
112
210
|
for i in PhotoSize:
|
|
113
211
|
client.remove_object(bucket, photo_path(album_key, photo_key, i.value))
|
|
114
212
|
|
|
115
213
|
|
|
116
214
|
def delete_scaled_photos(sizes):
|
|
117
|
-
client, bucket =
|
|
215
|
+
client, bucket = _photos_access()
|
|
118
216
|
|
|
119
217
|
for size in sizes:
|
|
120
|
-
|
|
121
|
-
objects = client.list_objects(
|
|
122
|
-
bucket,
|
|
123
|
-
prefix=f"{size}/",
|
|
124
|
-
recursive=True)
|
|
125
|
-
|
|
126
|
-
if not objects:
|
|
127
|
-
break
|
|
128
|
-
|
|
129
|
-
empty = True
|
|
130
|
-
for i in objects:
|
|
131
|
-
empty = False
|
|
132
|
-
client.remove_object(bucket, i.object_name)
|
|
133
|
-
yield i.object_name
|
|
134
|
-
|
|
135
|
-
if empty:
|
|
136
|
-
break
|
|
218
|
+
yield from _delete_all(client, bucket, f'{size}/')
|
|
137
219
|
|
|
138
220
|
|
|
139
221
|
def get_error_code(e: Exception) -> str:
|
|
@@ -151,22 +233,11 @@ def with_error_code(msg: str, e: Exception) -> str:
|
|
|
151
233
|
|
|
152
234
|
|
|
153
235
|
def put_photo_sizes(sizes: PhotoSizes):
|
|
154
|
-
|
|
155
|
-
stream = BytesIO(data.encode('utf-8'))
|
|
156
|
-
|
|
157
|
-
client, bucket = _objsto_access()
|
|
158
|
-
client.put_object(
|
|
159
|
-
bucket,
|
|
160
|
-
"photo_sizes.json",
|
|
161
|
-
stream,
|
|
162
|
-
length=-1,
|
|
163
|
-
part_size=10 * MEGABYTE,
|
|
164
|
-
content_type="application/json",
|
|
165
|
-
)
|
|
236
|
+
return _put_json("photo_sizes.json", asdict(sizes), _photos_access)
|
|
166
237
|
|
|
167
238
|
|
|
168
239
|
def get_photo_sizes() -> PhotoSizes:
|
|
169
|
-
client, bucket =
|
|
240
|
+
client, bucket = _photos_access()
|
|
170
241
|
try:
|
|
171
242
|
data = client.get_object(bucket, "photo_sizes.json")
|
|
172
243
|
return parse_photo_sizes(json.loads(data.read()))
|
photo_objects/django/signals.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
from django.db.models.signals import post_save, post_delete
|
|
1
|
+
from django.db.models.signals import post_save, post_delete, pre_delete
|
|
2
2
|
from django.dispatch import receiver
|
|
3
3
|
|
|
4
|
-
from .
|
|
4
|
+
from photo_objects.django.api import create_backup
|
|
5
|
+
from photo_objects.django.api.backup import delete_backup
|
|
6
|
+
|
|
7
|
+
from .models import Backup, Photo
|
|
5
8
|
|
|
6
9
|
|
|
7
10
|
@receiver(post_save, sender=Photo)
|
|
@@ -56,3 +59,19 @@ def update_album_on_photo_delete(sender, **kwargs):
|
|
|
56
59
|
|
|
57
60
|
if needs_save:
|
|
58
61
|
album.save()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@receiver(post_save, sender=Backup)
|
|
65
|
+
def create_backup_to_objsto(sender, **kwargs):
|
|
66
|
+
created = kwargs.get('created', False)
|
|
67
|
+
if not created:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
backup = kwargs.get('instance', None)
|
|
71
|
+
return create_backup(backup)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@receiver(pre_delete, sender=Backup)
|
|
75
|
+
def delete_backup_from_objsto(sender, **kwargs):
|
|
76
|
+
backup = kwargs.get('instance', None)
|
|
77
|
+
return delete_backup(backup)
|
|
@@ -14,7 +14,7 @@ from minio import S3Error
|
|
|
14
14
|
|
|
15
15
|
from photo_objects.django import objsto
|
|
16
16
|
from photo_objects.django.models import Album, Photo
|
|
17
|
-
from photo_objects.django.objsto import
|
|
17
|
+
from photo_objects.django.objsto import _photos_access
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def add_permissions(user, *permissions):
|
|
@@ -56,7 +56,7 @@ class TestCase(DjangoTestCase):
|
|
|
56
56
|
# pylint: disable=invalid-name
|
|
57
57
|
@classmethod
|
|
58
58
|
def tearDownClass(cls):
|
|
59
|
-
client, bucket =
|
|
59
|
+
client, bucket = _photos_access()
|
|
60
60
|
|
|
61
61
|
for i in client.list_objects(bucket, recursive=True):
|
|
62
62
|
client.remove_object(bucket, i.object_name)
|
photo_objects/utils.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import re
|
|
3
|
+
import unicodedata
|
|
1
4
|
from xml.etree import ElementTree as ET
|
|
2
5
|
|
|
3
6
|
from django.utils.safestring import mark_safe
|
|
@@ -22,3 +25,23 @@ def first_paragraph_textcontent(raw: str) -> str | None:
|
|
|
22
25
|
return None
|
|
23
26
|
|
|
24
27
|
return ''.join(first.itertext())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def timestamp_str(timestamp: datetime):
|
|
31
|
+
return timestamp.isoformat() if timestamp else None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def slugify(title: str, lower=False, replace_leading_underscores=False) -> str:
|
|
35
|
+
key = unicodedata.normalize(
|
|
36
|
+
'NFKD', title).encode(
|
|
37
|
+
'ascii', 'ignore').decode('ascii')
|
|
38
|
+
if lower:
|
|
39
|
+
key = key.lower()
|
|
40
|
+
|
|
41
|
+
key = re.sub(r'[^a-zA-Z0-9._-]', '-', key)
|
|
42
|
+
key = re.sub(r'[-_]{2,}', '-', key)
|
|
43
|
+
|
|
44
|
+
if replace_leading_underscores:
|
|
45
|
+
key = re.sub(r'^_+', '-', key)
|
|
46
|
+
|
|
47
|
+
return key
|
|
@@ -2,33 +2,36 @@ photo_objects/__init__.py,sha256=I1508w_ntomEqTFQgC74SurhxVXfCiDWZLRsny2f59g,60
|
|
|
2
2
|
photo_objects/config.py,sha256=0-Aeo-z-d_fxx-cjAjxSwPJZUgYaAi7NTodiErlxIXo,861
|
|
3
3
|
photo_objects/error.py,sha256=7afLYjxM0EaYioxVw_XUqHTvfSMSuQPUwwle0OVlaDY,45
|
|
4
4
|
photo_objects/img.py,sha256=2HVGS2g7rS2hnomozYL92oxrcN6zjDTHvWNr-UAqtGQ,4620
|
|
5
|
-
photo_objects/utils.py,sha256=
|
|
5
|
+
photo_objects/utils.py,sha256=V0THzeLSGkgLTWdVhP-ee1nb-vl051mtF4AQqdgMo88,1145
|
|
6
6
|
photo_objects/django/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
photo_objects/django/admin.py,sha256=
|
|
7
|
+
photo_objects/django/admin.py,sha256=pvyQKs2FMtKtSS2PE_NKR_jsSi7-GKz3bbsqDNgjt6w,352
|
|
8
8
|
photo_objects/django/apps.py,sha256=Apqu6o6fpoxda18NQgKupvQRvTAZxVviIK_-dUR3rck,1444
|
|
9
9
|
photo_objects/django/conf.py,sha256=ZpeIulEc1tpr8AO52meNKOF30Xf5osbDtDyHvQRtkx4,2593
|
|
10
10
|
photo_objects/django/context_processors.py,sha256=XacUmcYV-4NMMMNXPWrHKdvNd6lfyamisngaVerREiU,306
|
|
11
|
-
photo_objects/django/forms.py,sha256=
|
|
12
|
-
photo_objects/django/models.py,sha256=
|
|
13
|
-
photo_objects/django/objsto.py,sha256=
|
|
14
|
-
photo_objects/django/signals.py,sha256=
|
|
11
|
+
photo_objects/django/forms.py,sha256=zSSmdIYn4PEx1Nnw-tuo4h4otKj3C099lB4-vp2BRUc,6861
|
|
12
|
+
photo_objects/django/models.py,sha256=OxkkczIBg7TaFWWm4VdtbRMJOK_OQKrV29g-X2cm5BQ,7247
|
|
13
|
+
photo_objects/django/objsto.py,sha256=lYOd1PJzrcmqtqw-Xci85MX_9yWqzeisYx4uJjXhHdQ,6046
|
|
14
|
+
photo_objects/django/signals.py,sha256=_gb4vlZkeFNYWXxwhNreaUJoOsbIWvP8OovVLtzepaE,2161
|
|
15
15
|
photo_objects/django/urls.py,sha256=XnOSEB8YtAJamlEsjKYz6U1DfDu7HHZpAinpqdulR8k,2501
|
|
16
|
-
photo_objects/django/api/__init__.py,sha256=
|
|
16
|
+
photo_objects/django/api/__init__.py,sha256=51CRTiE975ufVhvI5x-M_2D28JP8FZWyLFiuV5EaQSg,120
|
|
17
17
|
photo_objects/django/api/album.py,sha256=CJDeGLCuYoxGUDcjssZRpFnToxG_KVE9Ii7NduFW2ks,2003
|
|
18
18
|
photo_objects/django/api/auth.py,sha256=lS0S1tMVH2uN30g4jlixklv3eMnQ2FbQVQvuRXeMGYo,1420
|
|
19
|
+
photo_objects/django/api/backup.py,sha256=Lq9fTjuoZssQG86QJFIxlLpQi2FU0AS8yCZklvu_ues,4379
|
|
19
20
|
photo_objects/django/api/photo.py,sha256=-lo1g6jfBr884wy-WV2DAEPxzH9V-tFUTRtitmA6i28,4471
|
|
20
21
|
photo_objects/django/api/photo_change_request.py,sha256=v94RA7SUM60tC9mIZdz8HppbNKfHWeTFNPr_BPw3pys,3075
|
|
21
22
|
photo_objects/django/api/utils.py,sha256=8r51YgFgKPD05Zjzhstl4jlQ4JM8DtsxUyAzhjXi5Pk,5567
|
|
22
23
|
photo_objects/django/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
24
|
photo_objects/django/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
25
|
photo_objects/django/management/commands/clean-scaled-photos.py,sha256=KJY6phgTCxcmbMUsUfCRQjatvCmKyFninM8zT-tB3Kc,2008
|
|
25
|
-
photo_objects/django/management/commands/create-initial-admin-account.py,sha256=
|
|
26
|
+
photo_objects/django/management/commands/create-initial-admin-account.py,sha256=PhC7jnRxsph8rML384vKfXzYfJqMQdd8oUHxUY7JEW8,1178
|
|
27
|
+
photo_objects/django/management/commands/restore-backup.py,sha256=pnlHWcEgFDlgKaslC5PdhVcm491uRylCD3ZdRwGS0B0,2157
|
|
26
28
|
photo_objects/django/migrations/0001_initial.py,sha256=BLW-EZ38sBgDhOYyprc-h_vuPpRxA11qxt4ZuYNO1Wo,2424
|
|
27
29
|
photo_objects/django/migrations/0002_created_at_updated_at.py,sha256=7OT2VvDffAkX9XKBHVY-jvzxeIl2yU0Jr1ByCNGcUfw,1039
|
|
28
30
|
photo_objects/django/migrations/0003_admin_visibility.py,sha256=PdxPOJzr-ViRBlOYUHEEGhe0hLtDysZJdMqvbjKVpEg,529
|
|
29
31
|
photo_objects/django/migrations/0004_camera_setup_and_settings.py,sha256=CS5xyIHgBE2Y7-PSJ52ffRQeCzs8p899px9upomk4O8,1844
|
|
30
32
|
photo_objects/django/migrations/0005_sitesettings.py,sha256=Ilf5vUwTFQfXVP37zz0NWo5dQdeHDh5e-MV3enm0ZKI,994
|
|
31
33
|
photo_objects/django/migrations/0006_photo_alt_text.py,sha256=5ZWR-bfS_72Mpb0SrvtnWzVME-zlcRPzDCofmFWkUeU,1003
|
|
34
|
+
photo_objects/django/migrations/0007_backup.py,sha256=tLHLAnojppT6mhFF3FokGdwDbIj40_yqO9qMFm0H3UI,649
|
|
32
35
|
photo_objects/django/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
36
|
photo_objects/django/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
37
|
photo_objects/django/templatetags/photo_objects_extras.py,sha256=1L6wweVA96rTWVXqgyf8gSey1wQKi01h6sqv9kZeTIY,1399
|
|
@@ -41,7 +44,7 @@ photo_objects/django/tests/test_og_meta.py,sha256=Kk5a9KvE88KZ60gLqXSe6rTz5YU-gd
|
|
|
41
44
|
photo_objects/django/tests/test_photo.py,sha256=JWXN3fF2VtuByMKm2o5b19HnxwDr6ecRwuGzgc5RsBw,13471
|
|
42
45
|
photo_objects/django/tests/test_photo_change_requests.py,sha256=Ld5ytqxxZiEWrqfX8htJ6-5ARU7tqTYD-iUhb7EMcnU,3078
|
|
43
46
|
photo_objects/django/tests/test_utils.py,sha256=zBLv8lkvcLMCaH4D6GR1KZqUe-rPowhcBkQX19-Kshs,2007
|
|
44
|
-
photo_objects/django/tests/utils.py,sha256=
|
|
47
|
+
photo_objects/django/tests/utils.py,sha256=LiObyRARkmO4arnY2gXNi_T8XxT9eSKKszENMo2UIh8,4639
|
|
45
48
|
photo_objects/django/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
49
|
photo_objects/django/views/utils.py,sha256=AjJK5r5HmTF63E9Q4W3pKggDESuhNXUvbROpS8m70KM,1319
|
|
47
50
|
photo_objects/django/views/api/__init__.py,sha256=GgywMJMSFmP5aoMEaYut5V666zachd5YFQIDBMr5znU,188
|
|
@@ -56,8 +59,8 @@ photo_objects/django/views/ui/photo.py,sha256=flwcET5nSChzfyAEWRTUlklTW2o64przNX
|
|
|
56
59
|
photo_objects/django/views/ui/photo_change_request.py,sha256=eaYGXFqtHj8qonDAmPyn4nrEHkL13EBD-1s8Phs0XP4,2098
|
|
57
60
|
photo_objects/django/views/ui/users.py,sha256=nb73cnzuV98QkJb0j8F2hqPgOGFIWpUFTFu6dXMeVwM,722
|
|
58
61
|
photo_objects/django/views/ui/utils.py,sha256=YV_YcUbX-zUkdFnBlezPChR6aPDhZJ9loSOHBSzF6Cc,273
|
|
59
|
-
photo_objects-0.
|
|
60
|
-
photo_objects-0.
|
|
61
|
-
photo_objects-0.
|
|
62
|
-
photo_objects-0.
|
|
63
|
-
photo_objects-0.
|
|
62
|
+
photo_objects-0.9.0.dist-info/licenses/LICENSE,sha256=V3w6hTjXfP65F4r_mejveHcV5igHrblxao3-2RlfVlA,1068
|
|
63
|
+
photo_objects-0.9.0.dist-info/METADATA,sha256=54uuCEzSyejSGgQ2AlWFlCjuwawJFHfLBxRE_EQ47D4,3605
|
|
64
|
+
photo_objects-0.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
65
|
+
photo_objects-0.9.0.dist-info/top_level.txt,sha256=SZeL8mhf-WMGdhRtTGFvZc3aIRBboQls9O0cFDMGdQ0,14
|
|
66
|
+
photo_objects-0.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|