Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions filecloudapi/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class AclEntryType(Enum):
user = "user"


ETag = str


@dataclass
class FileListEntry:
path: str
Expand All @@ -43,6 +46,7 @@ class FileListEntry:
isshareable: bool
issyncable: bool
isdatasyncable: bool
etag: ETag


@dataclass
Expand Down
53 changes: 39 additions & 14 deletions filecloudapi/fcserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
AclEntryType,
AclPermissions,
EntryType,
ETag,
FCShare,
FCShareGroup,
FCShareUser,
Expand Down Expand Up @@ -51,6 +52,8 @@ def str_to_bool(value):

log = logging.getLogger(__name__)

SEND_ETAG_HEADER = "X-FC-Send-ETag"


class Progress:
"""
Expand Down Expand Up @@ -129,11 +132,13 @@ def __init__(
else:
self.login()

def _api_call(self, method: str, params: Dict) -> ET.Element:
def _api_call(
self, method: str, params: Dict, headers: Optional[Dict] = None
) -> ET.Element:
"""
Perform a FC API call (post)
"""
resp = self.session.post(self.url + method, data=params)
resp = self.session.post(self.url + method, data=params, headers=headers)
resp.raise_for_status()
self.last_headers = resp.headers
return ET.fromstring(resp.content)
Expand Down Expand Up @@ -183,6 +188,15 @@ def _raise_exception_from_command(self, resp: ET.Element):
resp.findtext("./command/message", "")
)

def _extract_etag(self, etag: str):
"""
Extract ETag value from header
"""
if etag.startswith('"') and etag.endswith('"'):
return etag[1:-1]
else:
return etag

def login(self) -> None:
"""
Try to login to FC server with the credentials
Expand Down Expand Up @@ -305,6 +319,7 @@ def shared_opt(txt: Optional[str]) -> SharedType:
entry.findtext("./isshareable", "1") == "1",
entry.findtext("./issyncable", "1") == "1",
entry.findtext("./isdatasyncable", "1") == "1",
entry.findtext("./etag", ""),
)

def getfilelist(
Expand Down Expand Up @@ -368,7 +383,7 @@ def fileinfo_no_retry(self, path: str) -> FileListEntry:
"""
Returns information about file/directory 'path'
"""
resp = self._api_call("/core/fileinfo", {"file": path})
resp = self._api_call("/core/fileinfo", {"file": path, "includeextrafields": 1})

entry = resp.find("./entry")

Expand Down Expand Up @@ -598,7 +613,7 @@ def downloadfolder(self, path: str, dstPath: Union[pathlib.Path, str]) -> None:
else:
self.downloadfile(path + "/" + file.name, dstFn)

def deletefile(self, path: str, adminproxyuserid: Optional[str] = None):
def deletefile(self, path: str, adminproxyuserid: Optional[str] = None) -> None:
"""
Delete file at 'path'
"""
Expand All @@ -619,11 +634,11 @@ def upload_bytes(
nofileoverwrite: Optional[bool] = False,
iflastmodified: Optional[datetime.datetime] = None,
progress: Optional[Progress] = None,
) -> None:
) -> ETag:
"""
Upload bytes 'data' to server at 'serverpath'.
"""
self.upload(BufferedReader(BytesIO(data)), serverpath, datemodified, nofileoverwrite=nofileoverwrite, iflastmodified=iflastmodified, progress=progress) # type: ignore
return self.upload(BufferedReader(BytesIO(data)), serverpath, datemodified, nofileoverwrite=nofileoverwrite, iflastmodified=iflastmodified, progress=progress) # type: ignore

def upload_str(
self,
Expand All @@ -633,11 +648,11 @@ def upload_str(
nofileoverwrite: Optional[bool] = False,
iflastmodified: Optional[datetime.datetime] = None,
progress: Optional[Progress] = None,
) -> None:
) -> ETag:
"""
Upload str 'data' UTF-8 encoded to server at 'serverpath'.
"""
self.upload_bytes(
return self.upload_bytes(
data.encode("utf-8"),
serverpath,
datemodified,
Expand All @@ -655,12 +670,12 @@ def upload_file(
iflastmodified: Optional[datetime.datetime] = None,
adminproxyuserid: Optional[str] = None,
progress: Optional[Progress] = None,
) -> None:
) -> ETag:
"""
Upload file at 'localpath' to server at 'serverpath'.
"""
with open(localpath, "rb") as uploadf:
self.upload(
return self.upload(
uploadf,
serverpath,
datemodified,
Expand Down Expand Up @@ -689,7 +704,7 @@ def upload(
iflastmodified: Optional[datetime.datetime] = None,
adminproxyuserid: Optional[str] = None,
progress: Optional[Progress] = None,
) -> None:
) -> ETag:
"""
Upload seekable stream at uploadf to server at 'serverpath'
"""
Expand Down Expand Up @@ -833,6 +848,7 @@ def close(self):
resp = self.session.post(
self.url + "/core/upload?" + params_str,
files={"file_contents": (name, b"")},
headers={SEND_ETAG_HEADER: "1"},
)

resp.raise_for_status()
Expand All @@ -841,7 +857,7 @@ def close(self):
log.warning(f"Upload error. Response: {resp.text}")
raise ServerError("", resp.text)

return
return self._extract_etag(resp.headers["ETag"])

rf = RequestField(name="file_contents", data=data_marker, filename=name)
rf.make_multipart()
Expand All @@ -850,7 +866,7 @@ def close(self):

headers = {"Content-type": content_type}

while pos < data_size or (data_size == 0 and pos == 0):
while True:

curr_slice_size = min(slice_size, data_size - pos)
complete = 0 if pos + curr_slice_size < data_size else 1
Expand Down Expand Up @@ -881,6 +897,9 @@ def close(self):
"%%2FSHARED%2F%21", "%2FSHARED%2F!"
) # WEBUI DOES NOT ENCODE THE !

if complete == 1:
headers[SEND_ETAG_HEADER] = "1"

resp = self.session.post(
self.url + "/core/upload?" + params_str,
data=FileSlice(uploadf, pos, curr_slice_size, envelope),
Expand All @@ -899,6 +918,10 @@ def close(self):
if progress is not None:
progress.update(pos, data_size, True)

if complete == 1:
assert pos == data_size
return self._extract_etag(resp.headers["ETag"])

def share(self, path: str, adminproxyuserid: str = "") -> FCShare:
"""
Share 'path'
Expand Down Expand Up @@ -1048,7 +1071,7 @@ def createfolder(
path: str,
subpath: Optional[str] = None,
adminproxyuserid: Optional[str] = None,
) -> None:
) -> ETag:
"""
Create folder at 'path'
"""
Expand All @@ -1066,9 +1089,11 @@ def createfolder(
resp = self._api_call(
"/core/createfolder",
payload,
headers={SEND_ETAG_HEADER: "1"},
)

self._raise_exception_from_command(resp)
return self._extract_etag(self.last_headers["ETag"])

def renamefile(self, path: str, name: str, newname) -> None:
"""
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]

name = "filecloudapi-python"
version = "0.4.4"
version = "0.5.0"
description = "A Python library to connect to a Filecloud server"

packages = [{ include = "filecloudapi" }]
Expand Down