Skip to content

Commit 1992e3d

Browse files
committed
add support for multiple local and S3 storage backends
1 parent 0629e1d commit 1992e3d

File tree

25 files changed

+522
-382
lines changed

25 files changed

+522
-382
lines changed

demoapp/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ class DemoAppModel(models.Model):
99
null=True,
1010
blank=True,
1111
accept_mime_types=['image/*'],
12+
realm='admin',
1213
)

demoapp/settings.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,50 @@
9595
},
9696
}
9797

98+
STORAGES = {
99+
'default': {
100+
'BACKEND': 'django.core.files.storage.FileSystemStorage',
101+
},
102+
'staticfiles': {
103+
'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
104+
},
105+
'finder_public': {
106+
'BACKEND': 'finder.storage.FinderSystemStorage',
107+
'OPTIONS': {
108+
'location': BASE_DIR / 'workdir/media/filer_public',
109+
'base_url': '/media/filer_public/',
110+
'allow_overwrite': True,
111+
},
112+
},
113+
'finder_public_samples': {
114+
'BACKEND': 'finder.storage.FinderSystemStorage',
115+
'OPTIONS': {
116+
'location': BASE_DIR / 'workdir/media/filer_public_thumbnails',
117+
'base_url': '/media/filer_public_thumbnails/',
118+
'allow_overwrite': True,
119+
},
120+
},
121+
}
122+
123+
if False:
124+
STORAGES['finder_public'] = {
125+
'BACKEND': 'storages.backends.s3.S3Storage',
126+
'OPTIONS': {
127+
'access_key': 'finder',
128+
'secret_key': 'finderadmin',
129+
'bucket_name': 'finder-public',
130+
'endpoint_url': 'http://localhost:9000',
131+
},
132+
}
133+
STORAGES['finder_public_samples'] = {
134+
'BACKEND': 'storages.backends.s3.S3Storage',
135+
'OPTIONS': {
136+
'access_key': 'finder',
137+
'secret_key': 'finderadmin',
138+
'bucket_name': 'finder-public-samples',
139+
'endpoint_url': 'http://localhost:9000',
140+
},
141+
}
98142

99143
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
100144

finder/admin/file.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,14 @@ def replace_file(self, request, file_id):
7575
return HttpResponseNotFound(f"File {file_id} not found.")
7676
if request.content_type != 'multipart/form-data' or 'upload_file' not in request.FILES:
7777
return HttpResponseBadRequest(f"Content-Type {request.content_type} invalid for file upload.")
78+
realm = self.get_realm(request)
7879
uploaded_file = request.FILES['upload_file']
7980
if uploaded_file.content_type != file_obj.mime_type:
8081
return HttpResponseBadRequest(f"Can not replace file {file_obj.name} with different mime type.")
8182
# the payload of the file_obj is not replaced and remains orphaned, it may be deleted
8283
file_obj.file_name = file_obj.generate_filename(uploaded_file.name)
8384
file_obj.file_size = uploaded_file.size
84-
file_obj.receive_file(uploaded_file)
85+
file_obj.receive_file(realm, uploaded_file)
8586
file_obj.save()
8687
return HttpResponse(f"Replaced content of {file_obj.name} successfully.")
8788

@@ -113,10 +114,11 @@ def render_change_form(self, request, context, add=False, change=False, form_url
113114

114115
def get_editor_settings(self, request, inode):
115116
settings = super().get_editor_settings(request, inode)
117+
realm = self.get_realm(request)
116118
settings.update(
117119
base_url=reverse('admin:finder_filemodel_changelist', current_app=self.admin_site.name),
118-
download_url=inode.get_download_url(),
119-
thumbnail_url=inode.get_thumbnail_url(),
120+
download_url=inode.get_download_url(realm),
121+
thumbnail_url=inode.get_thumbnail_url(realm),
120122
file_id=inode.id,
121123
filename=inode.file_name,
122124
file_mime_type=inode.mime_type,

finder/admin/folder.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -219,17 +219,20 @@ def upload_file(self, request, folder_id):
219219
return HttpResponseNotFound(f"FolderModel<{folder_id}> not found.")
220220
if request.content_type != 'multipart/form-data' or 'upload_file' not in request.FILES:
221221
return HttpResponse("Bad encoding type or missing payload.", status=415)
222+
realm = self.get_realm(request)
222223
model = FileModel.objects.get_model_for(request.FILES['upload_file'].content_type)
223224
new_file = model.objects.create_from_upload(
225+
realm,
224226
request.FILES['upload_file'],
225227
folder=folder,
226228
owner=request.user,
227229
)
228-
return JsonResponse({'file_info': new_file.as_dict})
230+
return JsonResponse({'file_info': new_file.as_dict(realm)})
229231

230232
def update_inode(self, request, folder_id):
231233
if response := self.check_for_valid_post_request(request, folder_id):
232234
return response
235+
realm = self.get_realm(request)
233236
body = json.loads(request.body)
234237
try:
235238
inode_obj = self.get_object(request, body['id'])
@@ -252,7 +255,7 @@ def update_inode(self, request, folder_id):
252255
if update_values:
253256
inode_obj.save(update_fields=list(update_values.keys()))
254257
return JsonResponse({
255-
'new_inode': self.serialize_inode(inode_obj),
258+
'new_inode': self.serialize_inode(realm, inode_obj),
256259
'favorite_folders': self.get_favorite_folders(request, current_folder),
257260
})
258261

@@ -280,7 +283,10 @@ def move_inodes(self, request, folder_id):
280283
body = json.loads(request.body)
281284
current_folder = self.get_object(request, folder_id)
282285
if 'target_id' in body:
283-
target_folder = self.get_object(request, body['target_id'])
286+
if body['target_id'] == 'parent':
287+
target_folder = current_folder.parent
288+
else:
289+
target_folder = self.get_object(request, body['target_id'])
284290
if not target_folder:
285291
msg = gettext("Folder named “{folder}” not found.")
286292
return HttpResponseNotFound(msg.format(folder=body['target_id']))
@@ -315,23 +321,25 @@ def delete_inodes(self, request, folder_id):
315321
trash_folder = self.get_trash_folder(request)
316322
if current_folder.id == trash_folder.id:
317323
return HttpResponse("Cannot move inodes from trash folder into itself.", status=409)
324+
realm = self.get_realm(request)
318325
trash_ordering = trash_folder.get_max_ordering()
319326
inode_ids = body.get('inode_ids', [])
320327
for entry in FolderModel.objects.filter_unified(id__in=inode_ids):
321328
inode = FolderModel.objects.get_inode(id=entry['id'])
329+
update_fields = ['parent', 'ordering']
322330
if entry['is_folder']:
323331
PinnedFolder.objects.filter(folder=inode).delete()
324332
while trash_folder.listdir(name=inode.name, is_folder=True).exists():
325333
inode.name = f"{inode.name}.renamed"
326-
inode.save(update_fields=['name'])
334+
update_fields.append('name')
327335
DiscardedInode.objects.create(
328336
inode=inode.id,
329337
previous_parent=inode.parent,
330338
)
331339
trash_ordering += 1
332340
inode.ordering = trash_ordering
333341
inode.parent = trash_folder
334-
inode.save(update_fields=['parent'])
342+
inode.save(update_fields=update_fields)
335343
current_folder.reorder()
336344
return JsonResponse({
337345
'favorite_folders': self.get_favorite_folders(request, current_folder),
@@ -354,12 +362,16 @@ def undo_discarded_inodes(self, request):
354362
def erase_trash_folder(self, request):
355363
if request.method != 'DELETE':
356364
return HttpResponseNotAllowed(f"Method {request.method} not allowed. Only DELETE requests are allowed.")
365+
realm = self.get_realm(request)
357366
trash_folder_entries = self.get_trash_folder(request).listdir()
358367
DiscardedInode.objects.filter(inode__in=list(trash_folder_entries.values_list('id', flat=True))).delete()
359368
for entry in trash_folder_entries:
360-
# bulk delete does not work here because file must be erased from disk
369+
# bulk delete does not work here because each file must be erased from disk
361370
proxy_obj = InodeManager.get_proxy_object(entry)
362-
proxy_obj.delete()
371+
if proxy_obj.is_folder:
372+
proxy_obj.delete()
373+
else:
374+
proxy_obj.erase_and_delete(realm)
363375
fallback_folder = self.get_fallback_folder(request)
364376
return JsonResponse({
365377
'success_url': reverse(
@@ -373,6 +385,7 @@ def add_folder(self, request, folder_id):
373385
return response
374386
parent_folder = self.get_object(request, folder_id)
375387
assert parent_folder.is_folder
388+
realm = self.get_realm(request)
376389
body = json.loads(request.body)
377390
if parent_folder.listdir(name=body['name'], is_folder=True).exists():
378391
msg = gettext("A folder named “{name}” already exists.")
@@ -383,13 +396,14 @@ def add_folder(self, request, folder_id):
383396
owner=request.user,
384397
ordering=parent_folder.get_max_ordering() + 1,
385398
)
386-
return JsonResponse({'new_folder': self.serialize_inode(new_folder)})
399+
return JsonResponse({'new_folder': self.serialize_inode(realm, new_folder)})
387400

388401
def get_or_create_folder(self, request, folder_id):
389402
if response := self.check_for_valid_post_request(request, folder_id):
390403
return response
391404
if not (folder := self.get_object(request, folder_id)):
392405
return HttpResponseNotFound(f"FolderModel<{folder_id}> not found.")
406+
realm = self.get_realm(request)
393407
ordering = folder.get_max_ordering() + 1
394408
body = json.loads(request.body)
395409
for folder_name in body['relative_path'].split('/'):
@@ -400,4 +414,4 @@ def get_or_create_folder(self, request, folder_id):
400414
)
401415
if created:
402416
ordering += 1
403-
return JsonResponse({'folder': self.serialize_inode(folder)})
417+
return JsonResponse({'folder': self.serialize_inode(realm, folder)})

finder/admin/inode.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def toggle_pin(self, request, folder_id):
7878
'favorite_folders': self.get_favorite_folders(request, current_folder),
7979
})
8080

81-
def serialize_inode(self, inode):
81+
def serialize_inode(self, realm, inode):
8282
data = {field: inode.serializable_value(field) for field in inode.data_fields}
8383
data.update(
8484
owner_name=inode.owner.username if inode.owner else None,
@@ -88,8 +88,8 @@ def serialize_inode(self, inode):
8888
args=(inode.id,),
8989
current_app=self.admin_site.name,
9090
),
91-
download_url=inode.get_download_url(),
92-
thumbnail_url=inode.get_thumbnail_url(),
91+
download_url=inode.get_download_url(realm),
92+
thumbnail_url=inode.get_thumbnail_url(realm),
9393
summary=inode.summary,
9494
)
9595
if (inode.is_folder):
@@ -129,9 +129,10 @@ def get_inodes(self, request, **lookup):
129129
Return a serialized list of file/folder-s for the given folder.
130130
"""
131131
lookup = dict(lookup_by_label(request), **lookup)
132+
realm = self.get_realm(request)
132133
unified_queryset = FolderModel.objects.filter_unified(**lookup)
133134
unified_queryset = sort_by_attribute(request, unified_queryset)
134-
self.annotate_unified_queryset(unified_queryset)
135+
self.annotate_unified_queryset(realm, unified_queryset)
135136
return unified_queryset
136137

137138
def get_breadcrumbs(self, obj):
@@ -147,9 +148,9 @@ def get_breadcrumbs(self, obj):
147148
return breadcrumbs
148149

149150
def get_favorite_folders(self, request, current_folder):
150-
site = get_current_site(request)
151+
realm = self.get_realm(request)
151152
folders = PinnedFolder.objects.filter(
152-
realm__site=site,
153+
realm__site=realm.site,
153154
realm__slug=self.admin_site.name,
154155
owner=request.user,
155156
).values(
@@ -174,15 +175,15 @@ def get_favorite_folders(self, request, current_folder):
174175
for folder in folders:
175176
if folder['id'] == current_folder.id:
176177
if len(folders) == 0:
177-
folders.append(self.serialize_inode(fallback_folder))
178+
folders.append(self.serialize_inode(realm, fallback_folder))
178179
break
179180
else:
180181
if current_folder.id == root_folder.id:
181-
folders.insert(0, self.serialize_inode(current_folder))
182+
folders.insert(0, self.serialize_inode(realm, current_folder))
182183
elif current_folder.id != trash_folder.id:
183-
folders.append(self.serialize_inode(current_folder))
184+
folders.append(self.serialize_inode(realm, current_folder))
184185
if trash_folder.num_children > 0:
185-
inode_data = self.serialize_inode(trash_folder)
186+
inode_data = self.serialize_inode(realm, trash_folder)
186187
inode_data.update(is_trash=True)
187188
folders.append(inode_data)
188189
return folders
@@ -239,8 +240,8 @@ def get_menu_extension_settings(self, request):
239240
"""
240241
return {}
241242

242-
def annotate_unified_queryset(self, queryset):
243-
annotate_unified_queryset(queryset)
243+
def annotate_unified_queryset(self, realm, queryset):
244+
annotate_unified_queryset(realm, queryset)
244245
for entry in queryset:
245246
entry.update(
246247
change_url=reverse(

finder/browser/views.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from django.contrib.sites.shortcuts import get_current_site
22
from django.core.exceptions import BadRequest, ObjectDoesNotExist
3-
from django.core.files.storage import default_storage
43
from django.db.models import QuerySet, Subquery
54
from django.forms.renderers import DjangoTemplates
65
from django.http import JsonResponse, HttpResponseBadRequest, HttpResponseNotFound
@@ -68,7 +67,7 @@ def _get_realm(self, request, slug):
6867
raise ObjectDoesNotExist(f"Realm named {slug} not found for {site.domain}.")
6968

7069
@method_decorator(require_GET)
71-
def structure(self, request, slug):
70+
def structure(self, request, slug=None):
7271
realm = self._get_realm(request, slug)
7372
root_folder_id = str(realm.root_folder.id)
7473
request.session.setdefault('finder.open_folders', [])
@@ -126,7 +125,8 @@ def fetch(self, request, inode_id):
126125
'has_subfolders': inode.subfolders.exists(),
127126
}
128127
else:
129-
return inode.as_dict
128+
realm = inode.folder.get_realm()
129+
return inode.as_dict(realm)
130130

131131
@method_decorator(require_GET)
132132
def open(self, request, folder_id):
@@ -164,20 +164,22 @@ def list(self, request, folder_id):
164164
recursive = 'recursive' in request.GET
165165
lookup = lookup_by_label(request)
166166
lookup['mime_types'] = request.GET.getlist('mimetypes')
167+
current_folder = FolderModel.objects.get(id=folder_id)
167168
if recursive:
168-
descendants = FolderModel.objects.get(id=folder_id).descendants
169-
if isinstance(descendants, QuerySet):
170-
parent_ids = Subquery(descendants.values('id'))
169+
if isinstance(current_folder.descendants, QuerySet):
170+
parent_ids = Subquery(current_folder.descendants.values('id'))
171171
else:
172-
parent_ids = [descendant.id for descendant in descendants]
172+
parent_ids = [descendant.id for descendant in current_folder.descendants]
173173
unified_queryset = FileModel.objects.filter_unified(parent_id__in=parent_ids, is_folder=False, **lookup)
174174
else:
175175
unified_queryset = FileModel.objects.filter_unified(parent_id=folder_id, is_folder=False, **lookup)
176176
next_offset = offset + self.limit
177177
if next_offset >= unified_queryset.count():
178178
next_offset = None
179+
180+
realm = current_folder.get_realm()
179181
unified_queryset = sort_by_attribute(request, unified_queryset)
180-
annotate_unified_queryset(unified_queryset)
182+
annotate_unified_queryset(realm, unified_queryset)
181183
return {
182184
'files': list(unified_queryset[offset:offset + self.limit]),
183185
'offset': next_offset,
@@ -203,6 +205,7 @@ def search(self, request, folder_id):
203205
else: # django-cte not installed (slow)
204206
parent_ids = [descendant.id for descendant in starting_folder.descendants]
205207

208+
realm = starting_folder.get_realm()
206209
lookup = {
207210
'parent_id__in': parent_ids,
208211
'name_lower__icontains': search_query,
@@ -212,7 +215,7 @@ def search(self, request, folder_id):
212215
next_offset = offset + self.limit
213216
else:
214217
next_offset = None
215-
annotate_unified_queryset(unified_queryset)
218+
annotate_unified_queryset(realm, unified_queryset)
216219
return {
217220
'files': unified_queryset[offset:next_offset],
218221
'offset': next_offset,
@@ -226,16 +229,18 @@ def upload(self, request, folder_id):
226229
if request.content_type != 'multipart/form-data' or 'upload_file' not in request.FILES:
227230
raise BadRequest("Bad form encoding or missing payload.")
228231
model = FileModel.objects.get_model_for(request.FILES['upload_file'].content_type)
229-
folder = FolderModel.objects.get(id=folder_id)
232+
current_folder = FolderModel.objects.get(id=folder_id)
233+
realm = current_folder.get_realm()
230234
file = model.objects.create_from_upload(
235+
realm,
231236
request.FILES['upload_file'],
232-
folder=folder,
237+
folder=current_folder,
233238
owner=request.user,
234239
)
235240
form_class = file.get_form_class()
236241
form = form_class(instance=file, renderer=FormRenderer())
237242
response = {
238-
'file_info': file.as_dict,
243+
'file_info': file.as_dict(realm),
239244
'form_html': mark_safe(strip_spaces_between_tags(form.as_div())),
240245
}
241246
return response
@@ -251,11 +256,12 @@ def change(self, request, file_id):
251256
return {'file_info': None}
252257
if request.content_type != 'multipart/form-data':
253258
raise BadRequest("Bad form encoding or missing payload.")
259+
realm = file.folder.get_realm()
254260
form_class = file.get_form_class()
255261
form = form_class(instance=file, data=request.POST, renderer=FormRenderer())
256262
if form.is_valid():
257263
file = form.save()
258-
return {'file_info': file.as_dict}
264+
file.as_dict.cache_clear()
265+
return {'file_info': file.as_dict(realm)}
259266
else:
260267
return {'form_html': mark_safe(strip_spaces_between_tags(form.as_div()))}
261-

0 commit comments

Comments
 (0)