Skip to content

Commit

Permalink
[IMP] fs_file: Remove storage code; rename FSFileBytesIO -> FSFileValue
Browse files Browse the repository at this point in the history
  • Loading branch information
marielejeune authored and lmignon committed Aug 24, 2023
1 parent 40bcd18 commit 556c2ef
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 175 deletions.
182 changes: 162 additions & 20 deletions fs_file/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,159 @@
# pylint: disable=method-required-super
import base64
import itertools
from io import IOBase
import os.path
from io import BytesIO, IOBase

from odoo import fields

from .io import FSFileBytesIO
from odoo.addons.fs_attachment.models.ir_attachment import IrAttachment


class FSFileValue:
def __init__(
self,
attachment: IrAttachment = None,
name: str = None,
value: bytes | IOBase = None,
) -> None:
"""
This class holds the information related to FSFile field. It can be
used to assign a value to a FSFile field. In such a case, you can pass
the name and the file content as parameters.
When
:param attachment: the attachment to use to store the file.
:param name: the name of the file. If not provided, the name will be
taken from the attachment or the io.IOBase.
:param value: the content of the file. It can be bytes or an io.IOBase.
"""
self._is_new: bool = attachment is None
self._buffer: IOBase = None
self._attachment: IrAttachment = attachment
if name and attachment:
raise ValueError("Cannot set name and attachment at the same time")

Check warning on line 37 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L37

Added line #L37 was not covered by tests
if value:
if isinstance(value, IOBase):
self._buffer = value
if not hasattr(value, "name") and name:
self._buffer.name = name
elif not name:
raise ValueError(

Check warning on line 44 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L44

Added line #L44 was not covered by tests
"name must be set when value is an io.IOBase "
"and is not provided by the io.IOBase"
)
elif isinstance(value, bytes):
self._buffer = BytesIO(value)
if not name:
raise ValueError("name must be set when value is bytes")

Check warning on line 51 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L51

Added line #L51 was not covered by tests
self._buffer.name = name
else:
raise ValueError("value must be bytes or io.BytesIO")

Check warning on line 54 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L54

Added line #L54 was not covered by tests

@property
def write_buffer(self) -> BytesIO:
if self._buffer is None:
name = self._attachment.name if self._attachment else None
self._buffer = BytesIO()
self._buffer.name = name
return self._buffer

Check warning on line 62 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L59-L62

Added lines #L59 - L62 were not covered by tests

@property
def name(self) -> str | None:
name = (
self._attachment.name
if self._attachment
else self._buffer.name
if self._buffer
else None
)
if name:
return os.path.basename(name)
return None

Check warning on line 75 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L75

Added line #L75 was not covered by tests

@name.setter
def name(self, value: str) -> None:
# the name should only be updatable while the file is not yet stored
# TODO, we could also allow to update the name of the file and rename
# the file in the external file system
if self._is_new:
self.write_buffer.name = value

Check warning on line 83 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L83

Added line #L83 was not covered by tests
else:
raise ValueError(

Check warning on line 85 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L85

Added line #L85 was not covered by tests
"The name of the file can only be updated while the file is not "
"yet stored"
)

@property
def mimetype(self) -> str | None:
return self._attachment.mimetype if self._attachment else None

@property
def size(self) -> int:
return self._attachment.file_size if self._attachment else len(self._buffer)

@property
def url(self) -> str | None:
return self._attachment.url if self._attachment else None

Check warning on line 100 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L100

Added line #L100 was not covered by tests

@property
def internal_url(self) -> str | None:
return self._attachment.internal_url if self._attachment else None

@property
def attachment(self) -> IrAttachment | None:
return self._attachment

@attachment.setter
def attachment(self, value: IrAttachment) -> None:
self._attachment = value
self._buffer = None

Check warning on line 113 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L112-L113

Added lines #L112 - L113 were not covered by tests

@property
def read_buffer(self) -> BytesIO:
if self._buffer is None:
content = b""
name = None
if self._attachment:
content = self._attachment.raw
name = self._attachment.name
self._buffer = BytesIO(content)
self._buffer.name = name
return self._buffer

def getvalue(self) -> bytes:
buffer = self.read_buffer
current_pos = buffer.tell()
buffer.seek(0)
value = buffer.read()
buffer.seek(current_pos)
return value

def open(
self,
mode="rb",
block_size=None,
cache_options=None,
compression=None,
new_version=True,
**kwargs
) -> IOBase:
"""
Return a file-like object that can be used to read and write the file content.
See the documentation of open() into the ir.attachment model from the
fs_attachment module for more information.
"""
if not self._attachment:
raise ValueError("Cannot open a file that is not stored")

Check warning on line 150 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L150

Added line #L150 was not covered by tests
return self._attachment.open(
mode=mode,
block_size=block_size,
cache_options=cache_options,
compression=compression,
new_version=new_version,
**kwargs,
)


class FSFile(fields.Binary):
Expand All @@ -19,7 +167,7 @@ class FSFile(fields.Binary):
is not encoded in base64 but is a bytes object.
Moreover, the field is designed to always return an instance of
:class:`FSFileBytesIO` when reading the value. This class is a file-like
:class:`FSFileValue` when reading the value. This class is a file-like
object that can be used to read the file content and to get information
about the file (filename, mimetype, url, ...).
Expand All @@ -29,7 +177,7 @@ class FSFile(fields.Binary):
- a dict with the following keys:
- ``filename``: the filename of the file
- ``content``: the content of the file encoded in base64
- a FSFileBytesIO instance
- a FSFileValue instance
- a file-like object (e.g. an instance of :class:`io.BytesIO`)
When the value is provided is a bytes object the filename is set to the
Expand Down Expand Up @@ -60,10 +208,8 @@ class FSFile(fields.Binary):
type = "fs_file"

attachment: bool = True
storage_code: str = None

def __init__(self, storage_code: str = None, **kwargs):
self.storage_code = storage_code
def __init__(self, **kwargs):
kwargs["attachment"] = True
super().__init__(**kwargs)

Expand All @@ -74,7 +220,7 @@ def read(self, records):
("res_id", "in", records.ids),
]
data = {
att.res_id: FSFileBytesIO(attachment=att)
att.res_id: FSFileValue(attachment=att)
for att in records.env["ir.attachment"].sudo().search(domain)
}
records.env.cache.insert_missing(records, self, map(data.get, records._ids))
Expand All @@ -88,7 +234,6 @@ def create(self, record_values):
env["ir.attachment"]
.sudo()
.with_context(
storage_code=self.storage_code,
binary_field_real_user=env.user,
)
)
Expand All @@ -108,7 +253,7 @@ def create(self, record_values):
record.env.cache.update(
record,
self,
[FSFileBytesIO(attachment=attachment)],
[FSFileValue(attachment=attachment)],
dirty=False,
)

Expand All @@ -117,8 +262,6 @@ def write(self, records, value):
# with the following changes:
# - the value is not encoded in base64 and we therefore write on
# ir.attachment.raw instead of ir.attachment.datas
# - we use the storage_code to store the attachment in the right
# filesystem storage

# discard recomputation of self on records
records.env.remove_to_compute(self, records)
Expand All @@ -141,7 +284,6 @@ def write(self, records, value):
records.env["ir.attachment"]
.sudo()
.with_context(
storage_code=self.storage_code,
binary_field_real_user=records.env.user,
)
)
Expand Down Expand Up @@ -178,7 +320,7 @@ def write(self, records, value):
for att in created:
record = records.browse(att.res_id)
record.env.cache.update(
record, self, [FSFileBytesIO(attachment=att)], dirty=False
record, self, [FSFileValue(attachment=att)], dirty=False
)
else:
atts.unlink()

Check warning on line 326 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L326

Added line #L326 was not covered by tests
Expand All @@ -191,19 +333,19 @@ def _get_filename(self, record):
def convert_to_cache(self, value, record, validate=True):
if value is None or value is False:
return None
if isinstance(value, FSFileBytesIO):
if isinstance(value, FSFileValue):
return value
if isinstance(value, dict):
return FSFileBytesIO(
return FSFileValue(
name=value["filename"], value=base64.b64decode(value["content"])
)
if isinstance(value, IOBase):
name = getattr(value, "name", None)
if name is None:
name = self._get_filename(record)
return FSFileBytesIO(name=name, value=value)
return FSFileValue(name=name, value=value)
if isinstance(value, bytes):
return FSFileBytesIO(
return FSFileValue(
name=self._get_filename(record), value=base64.b64decode(value)
)
raise ValueError(

Check warning on line 351 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L351

Added line #L351 was not covered by tests
Expand Down Expand Up @@ -238,7 +380,7 @@ def __convert_to_record(self, value, record):
if isinstance(value, IOBase):
return value

Check warning on line 381 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L381

Added line #L381 was not covered by tests
if isinstance(value, bytes):
return FSFileBytesIO(value=value)
return FSFileValue(value=value)
raise ValueError(

Check warning on line 384 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L383-L384

Added lines #L383 - L384 were not covered by tests
"Invalid value for %s: %r\n"
"Should be base64 encoded bytes or a file-like object" % (self, value)
Expand All @@ -247,7 +389,7 @@ def __convert_to_record(self, value, record):
def convert_to_read(self, value, record, use_name_get=True):
if value is None or value is False:
return None

Check warning on line 391 in fs_file/fields.py

View check run for this annotation

Codecov / codecov/patch

fs_file/fields.py#L391

Added line #L391 was not covered by tests
if isinstance(value, FSFileBytesIO):
if isinstance(value, FSFileValue):
return {
"filename": value.name,
"url": value.internal_url,
Expand Down
Loading

0 comments on commit 556c2ef

Please sign in to comment.