Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
flask-admin
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
JIRA
JIRA
Merge Requests
0
Merge Requests
0
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Python-Dev
flask-admin
Commits
e56fe6ab
Commit
e56fe6ab
authored
Dec 21, 2015
by
Serge S. Koval
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1152 from pricez/storage
File admin refactoring to support multiple backends
parents
5e36cce3
1a4669df
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
149 additions
and
40 deletions
+149
-40
fileadmin.py
flask_admin/contrib/fileadmin.py
+149
-40
No files found.
flask_admin/contrib/fileadmin.py
View file @
e56fe6ab
from
datetime
import
datetime
import
os
import
os
import
os.path
as
op
import
os.path
as
op
import
platform
import
platform
import
re
import
re
import
shutil
import
shutil
from
datetime
import
datetime
from
operator
import
itemgetter
from
operator
import
itemgetter
from
werkzeug
import
secure_filename
from
werkzeug
import
secure_filename
from
flask
import
flash
,
redirect
,
abort
,
request
,
send_file
from
flask
import
flash
,
redirect
,
abort
,
request
,
send_file
...
@@ -19,6 +19,102 @@ from flask_admin.actions import action, ActionsMixin
...
@@ -19,6 +19,102 @@ from flask_admin.actions import action, ActionsMixin
from
flask_admin.babel
import
gettext
,
lazy_gettext
from
flask_admin.babel
import
gettext
,
lazy_gettext
class
LocalFileStorage
(
object
):
def
__init__
(
self
,
base_path
):
self
.
base_path
=
as_unicode
(
base_path
)
self
.
separator
=
os
.
sep
if
not
self
.
path_exists
(
self
.
base_path
):
raise
IOError
(
'FileAdmin path "
%
s" does not exist or is not accessible'
%
self
.
base_path
)
def
get_base_path
(
self
):
"""
Return base path. Override to customize behavior (per-user
directories, etc)
"""
return
op
.
normpath
(
self
.
base_path
)
def
make_dir
(
self
,
path
,
directory
):
"""
Creates a directory `directory` under the `path`
"""
os
.
mkdir
(
op
.
join
(
path
,
directory
))
def
get_files
(
self
,
path
,
directory
):
"""
Gets a list of tuples representing the files in the `directory`
under the `path`
:param path:
The path up to the directory
:param directory:
The directory that will have its files listed
Each tuple represents a file and it should contain the file name,
the relative path, a flag signifying if it is a directory, the file
size in bytes and the time last modified in seconds since the epoch
"""
items
=
[]
for
f
in
os
.
listdir
(
directory
):
fp
=
op
.
join
(
directory
,
f
)
rel_path
=
op
.
join
(
path
,
f
)
is_dir
=
self
.
is_dir
(
fp
)
size
=
op
.
getsize
(
fp
)
last_modified
=
op
.
getmtime
(
fp
)
items
.
append
((
f
,
rel_path
,
is_dir
,
size
,
last_modified
))
return
items
def
delete_tree
(
self
,
directory
):
"""
Deletes the directory `directory` and all its files and subdirectories
"""
shutil
.
rmtree
(
directory
)
def
delete_file
(
self
,
file_path
):
"""
Deletes the file located at `file_path`
"""
os
.
remove
(
file_path
)
def
path_exists
(
self
,
path
):
"""
Check if `path` exists
"""
return
op
.
exists
(
path
)
def
rename_path
(
self
,
src
,
dst
):
"""
Renames `src` to `dst`
"""
os
.
rename
(
src
,
dst
)
def
is_dir
(
self
,
path
):
"""
Check if `path` is a directory
"""
return
op
.
isdir
(
path
)
def
send_file
(
self
,
file_path
):
"""
Sends the file located at `file_path` to the user
"""
return
send_file
(
file_path
)
def
save_file
(
self
,
path
,
file_data
):
"""
Save uploaded file to the disk
:param path:
Path to save to
:param file_data:
Werkzeug `FileStorage` object
"""
file_data
.
save
(
path
)
class
FileAdmin
(
BaseView
,
ActionsMixin
):
class
FileAdmin
(
BaseView
,
ActionsMixin
):
"""
"""
Simple file-management interface.
Simple file-management interface.
...
@@ -171,7 +267,8 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -171,7 +267,8 @@ class FileAdmin(BaseView, ActionsMixin):
def
__init__
(
self
,
base_path
,
base_url
=
None
,
def
__init__
(
self
,
base_path
,
base_url
=
None
,
name
=
None
,
category
=
None
,
endpoint
=
None
,
url
=
None
,
name
=
None
,
category
=
None
,
endpoint
=
None
,
url
=
None
,
verify_path
=
True
,
menu_class_name
=
None
,
menu_icon_type
=
None
,
menu_icon_value
=
None
):
verify_path
=
True
,
menu_class_name
=
None
,
menu_icon_type
=
None
,
menu_icon_value
=
None
,
storage
=
LocalFileStorage
,
storage_args
=
None
):
"""
"""
Constructor.
Constructor.
...
@@ -191,8 +288,12 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -191,8 +288,12 @@ class FileAdmin(BaseView, ActionsMixin):
Verify if path exists. If set to `True` and path does not exist
Verify if path exists. If set to `True` and path does not exist
will raise an exception.
will raise an exception.
"""
"""
self
.
base_path
=
as_unicode
(
base_path
)
if
storage_args
is
None
:
storage_args
=
{}
storage_args
.
setdefault
(
'base_path'
,
base_path
)
self
.
base_url
=
base_url
self
.
base_url
=
base_url
self
.
storage
=
storage
(
**
storage_args
)
self
.
init_actions
()
self
.
init_actions
()
...
@@ -208,10 +309,6 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -208,10 +309,6 @@ class FileAdmin(BaseView, ActionsMixin):
not
isinstance
(
self
.
editable_extensions
,
set
)):
not
isinstance
(
self
.
editable_extensions
,
set
)):
self
.
editable_extensions
=
set
(
self
.
editable_extensions
)
self
.
editable_extensions
=
set
(
self
.
editable_extensions
)
# Check if path exists
if
not
op
.
exists
(
base_path
):
raise
IOError
(
'FileAdmin path "
%
s" does not exist or is not accessible'
%
base_path
)
super
(
FileAdmin
,
self
)
.
__init__
(
name
,
category
,
endpoint
,
url
,
super
(
FileAdmin
,
self
)
.
__init__
(
name
,
category
,
endpoint
,
url
,
menu_class_name
=
menu_class_name
,
menu_icon_type
=
menu_icon_type
,
menu_class_name
=
menu_class_name
,
menu_icon_type
=
menu_icon_type
,
menu_icon_value
=
menu_icon_value
)
menu_icon_value
=
menu_icon_value
)
...
@@ -232,7 +329,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -232,7 +329,7 @@ class FileAdmin(BaseView, ActionsMixin):
Return base path. Override to customize behavior (per-user
Return base path. Override to customize behavior (per-user
directories, etc)
directories, etc)
"""
"""
return
op
.
normpath
(
self
.
base_path
)
return
self
.
storage
.
get_base_path
(
)
def
get_base_url
(
self
):
def
get_base_url
(
self
):
"""
"""
...
@@ -425,14 +522,14 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -425,14 +522,14 @@ class FileAdmin(BaseView, ActionsMixin):
def
save_file
(
self
,
path
,
file_data
):
def
save_file
(
self
,
path
,
file_data
):
"""
"""
Save uploaded file to the
disk
Save uploaded file to the
storage
:param path:
:param path:
Path to save to
Path to save to
:param file_data:
:param file_data:
Werkzeug `FileStorage` object
Werkzeug `FileStorage` object
"""
"""
file_data
.
save
(
path
)
self
.
storage
.
save_file
(
path
,
file_data
)
def
validate_form
(
self
,
form
):
def
validate_form
(
self
,
form
):
"""
"""
...
@@ -493,12 +590,12 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -493,12 +590,12 @@ class FileAdmin(BaseView, ActionsMixin):
path
=
''
path
=
''
else
:
else
:
path
=
op
.
normpath
(
path
)
path
=
op
.
normpath
(
path
)
directory
=
op
.
normpath
(
op
.
join
(
base_path
,
path
))
directory
=
op
.
normpath
(
self
.
_separator
.
join
([
base_path
,
path
]
))
if
not
self
.
is_in_folder
(
base_path
,
directory
):
if
not
self
.
is_in_folder
(
base_path
,
directory
):
abort
(
404
)
abort
(
404
)
if
not
op
.
exists
(
directory
):
if
not
self
.
storage
.
path_
exists
(
directory
):
abort
(
404
)
abort
(
404
)
return
base_path
,
directory
,
path
return
base_path
,
directory
,
path
...
@@ -592,17 +689,32 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -592,17 +689,32 @@ class FileAdmin(BaseView, ActionsMixin):
pass
pass
def
_save_form_files
(
self
,
directory
,
path
,
form
):
def
_save_form_files
(
self
,
directory
,
path
,
form
):
filename
=
op
.
join
(
directory
,
filename
=
self
.
_separator
.
join
([
directory
,
secure_filename
(
form
.
upload
.
data
.
filename
)])
secure_filename
(
form
.
upload
.
data
.
filename
))
if
op
.
exists
(
filename
):
if
self
.
storage
.
path_
exists
(
filename
):
secure_name
=
op
.
join
(
path
,
secure_filename
(
form
.
upload
.
data
.
filename
)
)
secure_name
=
self
.
_separator
.
join
([
path
,
secure_filename
(
form
.
upload
.
data
.
filename
)]
)
raise
Exception
(
gettext
(
'File "
%(name)
s" already exists.'
,
raise
Exception
(
gettext
(
'File "
%(name)
s" already exists.'
,
name
=
secure_name
))
name
=
secure_name
))
else
:
else
:
self
.
save_file
(
filename
,
form
.
upload
.
data
)
self
.
save_file
(
filename
,
form
.
upload
.
data
)
self
.
on_file_upload
(
directory
,
path
,
filename
)
self
.
on_file_upload
(
directory
,
path
,
filename
)
@
property
def
_separator
(
self
):
return
self
.
storage
.
separator
def
_get_breadcrumbs
(
self
,
path
):
"""
Returns a list of tuples with each tuple containing the folder and
the tree up to that folder when traversing down the `path`
"""
accumulator
=
[]
breadcrumbs
=
[]
for
n
in
path
.
split
(
self
.
_separator
):
accumulator
.
append
(
n
)
breadcrumbs
.
append
((
n
,
self
.
_separator
.
join
(
accumulator
)))
return
breadcrumbs
@
expose
(
'/'
)
@
expose
(
'/'
)
@
expose
(
'/b/<path:path>'
)
@
expose
(
'/b/<path:path>'
)
def
index
(
self
,
path
=
None
):
def
index
(
self
,
path
=
None
):
...
@@ -619,7 +731,6 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -619,7 +731,6 @@ class FileAdmin(BaseView, ActionsMixin):
# Get path and verify if it is valid
# Get path and verify if it is valid
base_path
,
directory
,
path
=
self
.
_normalize_path
(
path
)
base_path
,
directory
,
path
=
self
.
_normalize_path
(
path
)
if
not
self
.
is_accessible_path
(
path
):
if
not
self
.
is_accessible_path
(
path
):
flash
(
gettext
(
'Permission denied.'
),
'error'
)
flash
(
gettext
(
'Permission denied.'
),
'error'
)
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
...
@@ -629,18 +740,16 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -629,18 +740,16 @@ class FileAdmin(BaseView, ActionsMixin):
# Parent directory
# Parent directory
if
directory
!=
base_path
:
if
directory
!=
base_path
:
parent_path
=
op
.
normpath
(
op
.
join
(
path
,
'..'
))
parent_path
=
op
.
normpath
(
self
.
_separator
.
join
([
path
,
'..'
]
))
if
parent_path
==
'.'
:
if
parent_path
==
'.'
:
parent_path
=
None
parent_path
=
None
items
.
append
((
'..'
,
parent_path
,
True
,
0
,
0
))
items
.
append
((
'..'
,
parent_path
,
True
,
0
,
0
))
for
f
in
os
.
listdir
(
directory
):
for
item
in
self
.
storage
.
get_files
(
path
,
directory
):
fp
=
op
.
join
(
directory
,
f
)
file_name
,
rel_path
,
is_dir
,
size
,
last_modified
=
item
rel_path
=
op
.
join
(
path
,
f
)
if
self
.
is_accessible_path
(
rel_path
):
if
self
.
is_accessible_path
(
rel_path
):
items
.
append
(
(
f
,
rel_path
,
op
.
isdir
(
fp
),
op
.
getsize
(
fp
),
op
.
getmtime
(
fp
))
)
items
.
append
(
item
)
# Sort by name
# Sort by name
items
.
sort
(
key
=
itemgetter
(
0
))
items
.
sort
(
key
=
itemgetter
(
0
))
...
@@ -652,11 +761,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -652,11 +761,7 @@ class FileAdmin(BaseView, ActionsMixin):
items
.
sort
(
key
=
lambda
values
:
(
values
[
0
],
values
[
1
],
values
[
2
],
values
[
3
],
datetime
.
fromtimestamp
(
values
[
4
])),
reverse
=
True
)
items
.
sort
(
key
=
lambda
values
:
(
values
[
0
],
values
[
1
],
values
[
2
],
values
[
3
],
datetime
.
fromtimestamp
(
values
[
4
])),
reverse
=
True
)
# Generate breadcrumbs
# Generate breadcrumbs
accumulator
=
[]
breadcrumbs
=
self
.
_get_breadcrumbs
(
path
)
breadcrumbs
=
[]
for
n
in
path
.
split
(
os
.
sep
):
accumulator
.
append
(
n
)
breadcrumbs
.
append
((
n
,
op
.
join
(
*
accumulator
)))
# Actions
# Actions
actions
,
actions_confirmation
=
self
.
get_actions_list
()
actions
,
actions_confirmation
=
self
.
get_actions_list
()
...
@@ -729,7 +834,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -729,7 +834,7 @@ class FileAdmin(BaseView, ActionsMixin):
base_url
=
urljoin
(
self
.
get_url
(
'.index'
),
base_url
)
base_url
=
urljoin
(
self
.
get_url
(
'.index'
),
base_url
)
return
redirect
(
urljoin
(
base_url
,
path
))
return
redirect
(
urljoin
(
base_url
,
path
))
return
send_file
(
directory
)
return
se
lf
.
storage
.
se
nd_file
(
directory
)
@
expose
(
'/mkdir/'
,
methods
=
(
'GET'
,
'POST'
))
@
expose
(
'/mkdir/'
,
methods
=
(
'GET'
,
'POST'
))
@
expose
(
'/mkdir/<path:path>'
,
methods
=
(
'GET'
,
'POST'
))
@
expose
(
'/mkdir/<path:path>'
,
methods
=
(
'GET'
,
'POST'
))
...
@@ -757,7 +862,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -757,7 +862,7 @@ class FileAdmin(BaseView, ActionsMixin):
if
self
.
validate_form
(
form
):
if
self
.
validate_form
(
form
):
try
:
try
:
os
.
mkdir
(
op
.
join
(
directory
,
form
.
name
.
data
)
)
self
.
storage
.
make_dir
(
directory
,
form
.
name
.
data
)
self
.
on_mkdir
(
directory
,
form
.
name
.
data
)
self
.
on_mkdir
(
directory
,
form
.
name
.
data
)
flash
(
gettext
(
'Successfully created directory:
%(directory)
s'
,
flash
(
gettext
(
'Successfully created directory:
%(directory)
s'
,
directory
=
form
.
name
.
data
))
directory
=
form
.
name
.
data
))
...
@@ -775,6 +880,12 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -775,6 +880,12 @@ class FileAdmin(BaseView, ActionsMixin):
return
self
.
render
(
template
,
form
=
form
,
dir_url
=
dir_url
,
return
self
.
render
(
template
,
form
=
form
,
dir_url
=
dir_url
,
header_text
=
gettext
(
'Create Directory'
))
header_text
=
gettext
(
'Create Directory'
))
def
delete_file
(
self
,
file_path
):
"""
Deletes the file located at `file_path`
"""
self
.
storage
.
delete_file
(
file_path
)
@
expose
(
'/delete/'
,
methods
=
(
'POST'
,))
@
expose
(
'/delete/'
,
methods
=
(
'POST'
,))
def
delete
(
self
):
def
delete
(
self
):
"""
"""
...
@@ -800,14 +911,13 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -800,14 +911,13 @@ class FileAdmin(BaseView, ActionsMixin):
flash
(
gettext
(
'Permission denied.'
),
'error'
)
flash
(
gettext
(
'Permission denied.'
),
'error'
)
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
if
op
.
is
dir
(
full_path
):
if
self
.
storage
.
is_
dir
(
full_path
):
if
not
self
.
can_delete_dirs
:
if
not
self
.
can_delete_dirs
:
flash
(
gettext
(
'Directory deletion is disabled.'
),
'error'
)
flash
(
gettext
(
'Directory deletion is disabled.'
),
'error'
)
return
redirect
(
return_url
)
return
redirect
(
return_url
)
try
:
try
:
self
.
before_directory_delete
(
full_path
,
path
)
self
.
before_directory_delete
(
full_path
,
path
)
s
hutil
.
rm
tree
(
full_path
)
s
elf
.
storage
.
delete_
tree
(
full_path
)
self
.
on_directory_delete
(
full_path
,
path
)
self
.
on_directory_delete
(
full_path
,
path
)
flash
(
gettext
(
'Directory "
%(path)
s" was successfully deleted.'
,
path
=
path
))
flash
(
gettext
(
'Directory "
%(path)
s" was successfully deleted.'
,
path
=
path
))
except
Exception
as
ex
:
except
Exception
as
ex
:
...
@@ -815,7 +925,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -815,7 +925,7 @@ class FileAdmin(BaseView, ActionsMixin):
else
:
else
:
try
:
try
:
self
.
before_file_delete
(
full_path
,
path
)
self
.
before_file_delete
(
full_path
,
path
)
os
.
remov
e
(
full_path
)
self
.
delete_fil
e
(
full_path
)
self
.
on_file_delete
(
full_path
,
path
)
self
.
on_file_delete
(
full_path
,
path
)
flash
(
gettext
(
'File "
%(name)
s" was successfully deleted.'
,
name
=
path
))
flash
(
gettext
(
'File "
%(name)
s" was successfully deleted.'
,
name
=
path
))
except
Exception
as
ex
:
except
Exception
as
ex
:
...
@@ -848,7 +958,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -848,7 +958,7 @@ class FileAdmin(BaseView, ActionsMixin):
flash
(
gettext
(
'Permission denied.'
),
'error'
)
flash
(
gettext
(
'Permission denied.'
),
'error'
)
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
if
not
op
.
exists
(
full_path
):
if
not
self
.
storage
.
path_
exists
(
full_path
):
flash
(
gettext
(
'Path does not exist.'
),
'error'
)
flash
(
gettext
(
'Path does not exist.'
),
'error'
)
return
redirect
(
return_url
)
return
redirect
(
return_url
)
...
@@ -856,8 +966,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -856,8 +966,7 @@ class FileAdmin(BaseView, ActionsMixin):
try
:
try
:
dir_base
=
op
.
dirname
(
full_path
)
dir_base
=
op
.
dirname
(
full_path
)
filename
=
secure_filename
(
form
.
name
.
data
)
filename
=
secure_filename
(
form
.
name
.
data
)
self
.
storage
.
rename_path
(
full_path
,
self
.
_separator
.
join
([
dir_base
,
filename
]))
os
.
rename
(
full_path
,
op
.
join
(
dir_base
,
filename
))
self
.
on_rename
(
full_path
,
dir_base
,
filename
)
self
.
on_rename
(
full_path
,
dir_base
,
filename
)
flash
(
gettext
(
'Successfully renamed "
%(src)
s" to "
%(dst)
s"'
,
flash
(
gettext
(
'Successfully renamed "
%(src)
s" to "
%(dst)
s"'
,
src
=
op
.
basename
(
path
),
src
=
op
.
basename
(
path
),
...
@@ -901,7 +1010,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -901,7 +1010,7 @@ class FileAdmin(BaseView, ActionsMixin):
flash
(
gettext
(
'Permission denied.'
),
'error'
)
flash
(
gettext
(
'Permission denied.'
),
'error'
)
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
dir_url
=
self
.
_get_dir_url
(
'.index'
,
o
s
.
path
.
dirname
(
path
))
dir_url
=
self
.
_get_dir_url
(
'.index'
,
o
p
.
dirname
(
path
))
next_url
=
next_url
or
dir_url
next_url
=
next_url
or
dir_url
form
=
self
.
edit_form
()
form
=
self
.
edit_form
()
...
@@ -974,7 +1083,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -974,7 +1083,7 @@ class FileAdmin(BaseView, ActionsMixin):
if
self
.
is_accessible_path
(
path
):
if
self
.
is_accessible_path
(
path
):
try
:
try
:
os
.
remov
e
(
full_path
)
self
.
delete_fil
e
(
full_path
)
flash
(
gettext
(
'File "
%(name)
s" was successfully deleted.'
,
name
=
path
))
flash
(
gettext
(
'File "
%(name)
s" was successfully deleted.'
,
name
=
path
))
except
Exception
as
ex
:
except
Exception
as
ex
:
flash
(
gettext
(
'Failed to delete file:
%(name)
s'
,
name
=
ex
),
'error'
)
flash
(
gettext
(
'Failed to delete file:
%(name)
s'
,
name
=
ex
),
'error'
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment