Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: extend File.write() to accept a rootnode and foldername #1308

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 74 additions & 27 deletions src/viur/core/modules/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
from viur.core.skeleton import SkeletonInstance, skeletonByKind
from viur.core.tasks import CallDeferred, DeleteEntitiesIter, PeriodicTask


# Globals for connectivity

VALID_FILENAME_REGEX = re.compile(
Expand Down Expand Up @@ -531,12 +530,12 @@

# Append additional parameters
if params := {
k: v for k, v in {
"download": download,
"filename": filename,
"options": options,
"size": size,
}.items() if v
k: v for k, v in {
"download": download,
"filename": filename,
"options": options,
"size": size,
}.items() if v
}:
serving_url += f"?{urlencode(params)}"

Expand Down Expand Up @@ -708,45 +707,93 @@
width: int = None,
height: int = None,
public: bool = False,
foldername: t.Optional[str | t.Iterable[str]] = None,
rootnode: t.Optional[db.Key] = None,
) -> db.Key:
"""
Write a file from any buffer into the file module.
Write a file from any bytes-like object into the file module.

If foldername and rootnode are both set, the file is added to the repository in that folder.
If only foldername is set, the file is added to the default repository in that folder.
If only rootnode is set, the file is added to that repository in the root folder.
If both are not set, the file is added without a path or repository. It will not be visible in admin.

:param filename: Filename to be written.
:param content: The file content to be written, as bytes-like object.
:param mimetype: The file's mimetype.
:param width: Optional width information for the file.
:param height: Optional height information for the file.
:param public: True if the file should be publicly accessible.
:param foldername: Optional folder the file should be written into.
:param rootnode: Optional root-node of the repository to add the file to
:return: Returns the key of the file object written. This can be associated e.g. with a FileBone.
"""
if not File.is_valid_filename(filename):
if not self.is_valid_filename(filename):
raise ValueError(f"{filename=} is invalid")

# Check for foldername
if rootnode is None:
rootnode = self.ensureOwnModuleRootNode()
elif not foldername:
# if rootnode is set and foldername is not, save the file in the root of the rootnode
foldername = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like tuples ;)

Suggested change
foldername = []
foldername = ()


if foldername is not None:
foldernames = foldername
if isinstance(foldername, str):
foldernames = foldernames.replace('\\', '/')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should handle this case. What do the others think? @sveneberth?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this write method is only for internal usage and we don't expect pathes from client users I don't see the necessity of it.

foldernames = (i for i in foldernames.split('/'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foldernames = (i for i in foldernames.split('/'))
foldernames = foldernames.split("/")

foldernames = (i for i in foldernames if i)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foldernames = (i for i in foldernames if i)
foldernames = tuple(foldernames) # ensure tuple, run any iterators


parentrepokey = rootnode.key
parentfolderkey = rootnode.key

for fname in foldernames:
currentfolder = (self.addSkel("node").all()
.filter("parentrepo", parentrepokey)
.filter("parententry", parentfolderkey)
.filter("name", fname)
.getSkel())
if currentfolder:
parentfolderkey = currentfolder.key
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since getSkel() returns an SkeletonInstance, skel.key would be a the KeyBone key (or am I wrong?).

Suggested change
parentfolderkey = currentfolder.key
parentfolderkey = currentfolder["key"]

else:
newfolderskel = self.addSkel("node")
newfolderskel["name"] = fname
newfolderskel["parentrepo"] = parentrepokey
newfolderskel["parententry"] = parentfolderkey
newfolderskel.write()
parentfolderkey = newfolderskel["key"]

# Save the file into the folder.
dl_key = utils.string.random()

if public:
dl_key += PUBLIC_DLKEY_SUFFIX # mark file as public

bucket = File.get_bucket(dl_key)
bucket = self.get_bucket(dl_key)

blob = bucket.blob(f"{dl_key}/source/{filename}")
blob.upload_from_file(io.BytesIO(content), content_type=mimetype)

skel = self.addSkel("leaf")
skel["name"] = filename
skel["size"] = blob.size
skel["mimetype"] = mimetype
skel["dlkey"] = dl_key
skel["weak"] = True
skel["public"] = public
skel["width"] = width
skel["height"] = height
skel["crc32c_checksum"] = base64.b64decode(blob.crc32c).hex()
skel["md5_checksum"] = base64.b64decode(blob.md5_hash).hex()

skel.write()
return skel["key"]
fileskel = self.addSkel("leaf")
fileskel["name"] = filename
fileskel["size"] = blob.size
fileskel["mimetype"] = mimetype
fileskel["dlkey"] = dl_key
fileskel["weak"] = foldername is None
fileskel["public"] = public
fileskel["width"] = width
fileskel["height"] = height
fileskel["crc32c_checksum"] = base64.b64decode(blob.crc32c).hex()
fileskel["md5_checksum"] = base64.b64decode(blob.md5_hash).hex()

bb-mausbrand marked this conversation as resolved.
Show resolved Hide resolved
if foldername is not None:
fileskel["parentrepo"] = parentrepokey
fileskel["parententry"] = parentfolderkey
fileskel["pending"] = False
bb-mausbrand marked this conversation as resolved.
Show resolved Hide resolved

fileskel.write()
return fileskel["key"]

def read(
self,
Expand Down Expand Up @@ -1210,7 +1257,7 @@
"""Inject the serving url for public image files into a FileSkel"""
# try to create a servingurl for images
if not conf.instance.is_dev_server and skel["public"] and skel["mimetype"] \
and skel["mimetype"].startswith("image/") and not skel["serving_url"]:
and skel["mimetype"].startswith("image/") and not skel["serving_url"]:

try:
bucket = File.get_bucket(skel['dlkey'])
Expand Down Expand Up @@ -1260,7 +1307,7 @@
return
fileObj = db.Entity(db.Key("viur-deleted-files"))
fileObj["itercount"] = 0
fileObj["dlkey"] = str(blobKey)

Check failure on line 1310 in src/viur/core/modules/file.py

View workflow job for this annotation

GitHub Actions / linter (3.12)

E125: continuation line with same indent as next logical line
logging.info(f"Stale blob marked dirty, {blobKey}")
db.Put(fileObj)
newCursor = query.getCursor()
Expand Down Expand Up @@ -1332,8 +1379,8 @@

def __getattr__(attr: str) -> object:
if entry := {
# stuff prior viur-core < 3.7
"GOOGLE_STORAGE_BUCKET": ("File.get_bucket()", _private_bucket),
# stuff prior viur-core < 3.7
"GOOGLE_STORAGE_BUCKET": ("File.get_bucket()", _private_bucket),
}.get(attr):
msg = f"{attr} was replaced by {entry[0]}"
warnings.warn(msg, DeprecationWarning, stacklevel=2)
Expand Down
Loading