Skip to content

Commit 659f8dc

Browse files
authored
more consistent fs api and tests (#8)
1 parent 75e6c68 commit 659f8dc

File tree

11 files changed

+229
-57
lines changed

11 files changed

+229
-57
lines changed

CHANGELOG.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,18 @@ _# Changelog
22

33
All notable changes to this project will be documented in this file.
44

5-
## [0.0.7 - 2022-12-12]
5+
## [0.0.7 - 2022-12-14]
66

77
### Added
88

99
- FS functions:
10+
* `fs_node_info`
11+
* `fs_list_directory`
12+
* `fs_file_data`
1013
* `fs_apply_exclude_lists`
1114
* `fs_apply_ignore_flags`
1215
* `fs_extract_sub_dirs`
1316
* `fs_filter_by`
14-
* `fs_get_file_data`
15-
* `fs_get_obj_info`
16-
* `fs_get_objs_info`
17-
* `fs_list_directory`
1817
* `fs_sort_by_id`
1918

2019
### Changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
![PythonVersion](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11-blue)
55
![impl](https://img.shields.io/pypi/implementation/nc_py_api)
66
![pypi](https://img.shields.io/pypi/v/nc_py_api.svg)
7+
[![codecov](https://codecov.io/gh/cloud-py-api/cloud_py_api/branch/main/graph/badge.svg?token=6IHPKUYUU9)](https://codecov.io/gh/cloud-py-api/cloud_py_api)
78

89
Framework(App) for Nextcloud to develop apps, that using Python.
910

nc_py_api/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@
99
fs_apply_exclude_lists,
1010
fs_apply_ignore_flags,
1111
fs_extract_sub_dirs,
12+
fs_file_data,
1213
fs_filter_by,
13-
fs_get_file_data,
14-
fs_get_obj_info,
15-
fs_get_objs_info,
1614
fs_list_directory,
15+
fs_node_info,
1716
fs_sort_by_id,
1817
)
1918
from .log import cpa_logger

nc_py_api/db_requests.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
FIELD_NAME_LIST = (
88
"fcache.fileid, fcache.storage, fcache.path, fcache.storage, fcache.name, "
9-
"fcache.mimetype, fcache.mimepart, "
9+
"fcache.mimetype, fcache.mimepart, fcache.parent, "
1010
"fcache.size, fcache.mtime, fcache.encrypted, fcache.etag, fcache.permissions, fcache.checksum"
1111
)
1212

@@ -77,6 +77,19 @@ def get_fileid_info(file_id: int) -> dict:
7777
return {}
7878

7979

80+
def get_fs_obj_info_by_path(obj_path: str, storage_numeric_id: int) -> dict:
81+
"""Returns dictionary with information for given userid:path."""
82+
83+
query = (
84+
f"SELECT {FIELD_NAME_LIST} FROM {TABLES.file_cache} AS fcache "
85+
f"WHERE fcache.path = '{obj_path}' AND fcache.storage = {storage_numeric_id};"
86+
)
87+
result = execute_fetchall(query)
88+
if result:
89+
return result[0]
90+
return {}
91+
92+
8093
def get_fileids_info(file_ids: list[int]) -> list[dict]:
8194
"""Returns dictionaries with information for given file ids."""
8295

nc_py_api/files.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
get_directory_list,
1313
get_fileid_info,
1414
get_fileids_info,
15+
get_fs_obj_info_by_path,
1516
get_non_direct_access_filesize_limit,
1617
get_paths_by_ids,
1718
get_storages_info,
@@ -30,6 +31,7 @@ class FsNodeInfo(TypedDict):
3031
internal_path: str
3132
abs_path: str
3233
size: int
34+
parent_id: int
3335
permissions: int
3436
mtime: int
3537
checksum: str
@@ -50,20 +52,33 @@ class FsNodeInfo(TypedDict):
5052
"""A value from the config that defines the maximum file size allowed to be requested from php."""
5153

5254

53-
def fs_get_obj_info(file_id: int) -> Optional[FsNodeInfo]:
54-
raw_result = get_fileid_info(file_id)
55+
def fs_node_info(obj: Union[list[int], int, str], user_id=USER_ID) -> Union[list[FsNodeInfo], Optional[FsNodeInfo]]:
56+
"""Gets `FsNodeInfo` by list of ids, id or path.
57+
58+
:param obj: for the list of ints or one int it is a `fileid` value. For ``str`` type it is the
59+
relative path to file/directory. `path` field from NC DB, without `files/` prefix.
60+
:param user_id: `uid` of user. Optional, in most cases you should not specify it.
61+
62+
:returns: list of :py:data:`FsNodeInfo`, :py:data:`FsNodeInfo` or None in case of error.
63+
Depends on the type of `obj` parameter."""
64+
65+
if isinstance(obj, list):
66+
return [db_record_to_fs_node(i) for i in get_fileids_info(obj)]
67+
if isinstance(obj, int):
68+
raw_result = get_fileid_info(obj)
69+
else:
70+
numeric_id = get_storage_by_user_id(user_id).get("numeric_id", 0)
71+
if not numeric_id:
72+
log.debug("can not find storage for specified user: %s", user_id)
73+
return None
74+
raw_result = get_fs_obj_info_by_path(path.join("files", obj.lstrip("/")).rstrip("/"), numeric_id)
5575
if raw_result:
5676
return db_record_to_fs_node(raw_result)
5777
return None
5878

5979

60-
def fs_get_objs_info(file_ids: list[int]) -> list[FsNodeInfo]:
61-
raw_result = get_fileids_info(file_ids)
62-
return [db_record_to_fs_node(i) for i in raw_result]
63-
64-
6580
def fs_list_directory(file_id: Optional[Union[int, FsNodeInfo]] = None, user_id=USER_ID) -> list[FsNodeInfo]:
66-
"""Get listing of the directory.
81+
"""Gets listing of the directory.
6782
6883
:param file_id: `fileid` or :py:data:`FsNodeInfo` of the directory. Can be `None` to list `root` directory.
6984
:param user_id: `uid` of user. Optional, in most cases you should not specify it.
@@ -141,7 +156,7 @@ def fs_sort_by_id(fs_objs: list[FsNodeInfo]) -> list[FsNodeInfo]:
141156
return sorted(fs_objs, key=lambda i: i["id"])
142157

143158

144-
def fs_get_file_data(file_info: FsNodeInfo) -> bytes:
159+
def fs_file_data(file_info: FsNodeInfo) -> bytes:
145160
if file_info["direct_access"]:
146161
try:
147162
with open(file_info["abs_path"], "rb") as h_file:
@@ -255,6 +270,7 @@ def db_record_to_fs_node(fs_record: dict) -> FsNodeInfo:
255270
"internal_path": fs_record["path"],
256271
"abs_path": get_file_full_path(fs_record["storage"], fs_record["path"]),
257272
"size": fs_record["size"],
273+
"parent_id": fs_record["parent"],
258274
"permissions": fs_record["permissions"],
259275
"mtime": fs_record["mtime"],
260276
"checksum": fs_record["checksum"],

tests/nc_py_api/files_test.py

Lines changed: 0 additions & 38 deletions
This file was deleted.

tests/nc_py_api/fs_test.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import logging
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
from nc_py_api import (
7+
fs_file_data,
8+
fs_list_directory,
9+
fs_node_info,
10+
get_mimetype_id,
11+
mimetype,
12+
)
13+
14+
15+
@pytest.mark.parametrize("test_path", ["", "/"])
16+
@pytest.mark.parametrize("user_id", ["admin"])
17+
def test_node_info_root(test_path, user_id):
18+
root_dir_info = fs_node_info(test_path, user_id=user_id)
19+
assert root_dir_info
20+
assert root_dir_info["mimetype"] == mimetype.DIR
21+
assert root_dir_info["is_dir"]
22+
assert root_dir_info["name"] == "files"
23+
24+
25+
@pytest.mark.parametrize("test_path", ["Documents", "Photos", "Templates", "test_dir"])
26+
def test_node_info_path_dirs(test_path):
27+
dir_info = fs_node_info(test_path, user_id="admin")
28+
assert dir_info
29+
assert dir_info["mimetype"] == mimetype.DIR
30+
assert dir_info["is_dir"]
31+
assert dir_info["name"] == test_path
32+
33+
34+
@pytest.mark.parametrize("test_path", ["test_dir/hopper.png", "/test_dir/复杂 目录 Í/empty_file.bin"])
35+
def test_node_info_path_files(test_path):
36+
file_info = fs_node_info(test_path, user_id="admin")
37+
assert file_info
38+
assert not file_info["is_dir"]
39+
assert file_info["name"] == Path(test_path).parts[-1:][0]
40+
41+
42+
@pytest.mark.parametrize("test_path", ["/test_dir/empty_dir/", "test_dir/empty_dir/", "/test_dir/empty_dir"])
43+
def test_node_info_path_slashes(test_path):
44+
dir_info = fs_node_info(test_path, user_id="admin")
45+
assert dir_info
46+
assert dir_info["mimetype"] == mimetype.DIR
47+
assert dir_info["is_dir"]
48+
assert dir_info["name"] == "empty_dir"
49+
50+
51+
@pytest.mark.parametrize("test_path", ["/test_dir/复杂 目录 Í/", "test_dir/复杂 目录 Í/", "/test_dir/复杂 目录 Í"])
52+
def test_node_info_path_diff_symbols(test_path):
53+
dir_info = fs_node_info(test_path, user_id="admin")
54+
assert dir_info
55+
assert dir_info["mimetype"] == mimetype.DIR
56+
assert dir_info["is_dir"]
57+
assert dir_info["name"] == "复杂 目录 Í"
58+
59+
60+
@pytest.mark.parametrize("test_path", ["*-1", "no path", "/no path", "no path/"])
61+
@pytest.mark.parametrize("user_id", ["", None, "non_exist"])
62+
def test_node_info_invalid_input(test_path, user_id):
63+
logging.disable(logging.CRITICAL)
64+
path_info = fs_node_info(test_path, user_id=user_id)
65+
logging.disable(logging.NOTSET)
66+
assert path_info is None
67+
68+
69+
@pytest.mark.parametrize("user_id", ["admin"])
70+
def test_list_directory_root(user_id):
71+
root_dir_listing = fs_list_directory(user_id=user_id)
72+
# `Documents`, `Photos`, `Templates`, `test_files` folders
73+
assert len(root_dir_listing) >= 4
74+
assert any(fs_obj["name"] == "Documents" for fs_obj in root_dir_listing)
75+
assert any(fs_obj["name"] == "Photos" for fs_obj in root_dir_listing)
76+
assert any(fs_obj["name"] == "Templates" for fs_obj in root_dir_listing)
77+
assert any(fs_obj["name"] == "test_dir" for fs_obj in root_dir_listing)
78+
79+
80+
@pytest.mark.parametrize("file_id", [None, 0, 18446744073709551610])
81+
@pytest.mark.parametrize("user_id", ["", None, "non_exist"])
82+
def test_list_directory_invalid_input(file_id, user_id):
83+
logging.disable(logging.CRITICAL)
84+
root_dir_listing = fs_list_directory(file_id=file_id, user_id=user_id)
85+
logging.disable(logging.NOTSET)
86+
assert isinstance(root_dir_listing, list)
87+
assert not root_dir_listing
88+
89+
90+
def test_list_directory_test_dir():
91+
test_dir_listing = fs_list_directory(fs_node_info("test_dir", user_id="admin"), user_id="admin")
92+
assert len(test_dir_listing) == 4
93+
assert any(fs_obj["name"] == "empty_dir" for fs_obj in test_dir_listing)
94+
assert any(fs_obj["name"] == "复杂 目录 Í" for fs_obj in test_dir_listing)
95+
assert any(fs_obj["name"] == "hopper.png" for fs_obj in test_dir_listing)
96+
assert any(fs_obj["name"] == "test.txt" for fs_obj in test_dir_listing)
97+
98+
99+
@pytest.mark.parametrize("test_path", ["test_dir", "test_dir/hopper.png", "/test_dir/复杂 目录 Í/empty_file.bin"])
100+
def test_node_info_id(test_path):
101+
file_info1 = fs_node_info(test_path, user_id="admin")
102+
file_info2 = fs_node_info(file_info1["id"])
103+
assert isinstance(file_info1, dict)
104+
assert file_info1 == file_info2
105+
106+
107+
def test_node_info_ids():
108+
test_dir_listing = fs_list_directory(fs_node_info("test_dir", user_id="admin"), user_id="admin")
109+
ids = [i["id"] for i in test_dir_listing]
110+
objs_info = fs_node_info(ids)
111+
assert isinstance(objs_info, list)
112+
assert len(objs_info) == 4
113+
assert all(obj["id"] for obj in objs_info)
114+
115+
116+
def test_parent_field():
117+
dir_info = fs_node_info("test_dir/empty_dir", user_id="admin")
118+
parent_info = fs_node_info(dir_info["parent_id"])
119+
assert parent_info["name"] == "test_dir"
120+
dir_info = fs_node_info("test_dir/hopper.png", user_id="admin")
121+
parent_info = fs_node_info(dir_info["parent_id"])
122+
assert parent_info["name"] == "test_dir"
123+
dir_info = fs_node_info("test_dir/empty_dir/empty_file.bin", user_id="admin")
124+
parent_info = fs_node_info(dir_info["parent_id"])
125+
assert parent_info["name"] == "empty_dir"
126+
127+
128+
def test_node_dir_fields():
129+
dir_info = fs_node_info("test_dir/empty_dir", user_id="admin")
130+
_ = fs_list_directory(fs_node_info("test_dir", user_id="admin"), user_id="admin")
131+
dir_info2 = [i for i in _ if i["name"] == "empty_dir"][0]
132+
assert dir_info == dir_info2
133+
assert dir_info["is_dir"]
134+
assert dir_info["is_local"]
135+
assert dir_info["mimetype"] == mimetype.DIR
136+
assert dir_info["mimepart"] == get_mimetype_id("httpd")
137+
assert dir_info["internal_path"] == "files/test_dir/empty_dir"
138+
assert dir_info["permissions"] == 31
139+
assert dir_info["ownerName"] == "admin"
140+
assert dir_info["size"] == 0
141+
142+
143+
def test_node_file_fields():
144+
file_info = fs_node_info("test_dir/hopper.png", user_id="admin")
145+
_ = fs_list_directory(fs_node_info("test_dir", user_id="admin"), user_id="admin")
146+
file_info2 = [i for i in _ if i["name"] == "hopper.png"][0]
147+
assert file_info == file_info2
148+
assert not file_info["is_dir"]
149+
assert file_info["is_local"]
150+
assert file_info["mimetype"] == get_mimetype_id("image/png")
151+
assert file_info["mimepart"] == mimetype.IMAGE
152+
assert file_info["internal_path"] == "files/test_dir/hopper.png"
153+
assert file_info["permissions"] == 27
154+
assert file_info["ownerName"] == "admin"
155+
assert file_info["size"] == 30605
156+
157+
158+
@pytest.mark.parametrize("file", ["test_dir/hopper.png", "test_dir/test.txt", "/test_dir/复杂 目录 Í/empty_file.bin"])
159+
def test_fs_file_data(file):
160+
node_info = fs_node_info(file, user_id="admin")
161+
file_data = fs_file_data(node_info)
162+
assert isinstance(file_data, bytes)
163+
if file.find("empty") == -1:
164+
assert file_data
165+
else:
166+
assert not len(file_data)

tests/nc_py_api/mimetype_test.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import logging
2+
13
import pytest
24

3-
from nc_py_api import get_mimetype_id
5+
from nc_py_api import fs_node_info, get_mimetype_id
46

57

68
@pytest.mark.parametrize("mimetype", ["httpd", "httpd/unix-directory", "application", "text", "image", "video"])
@@ -10,4 +12,15 @@ def test_get_mimetype_id(mimetype):
1012

1113
@pytest.mark.parametrize("mimetype", ["", "invalid_mime", None, "'invalid_mime'"])
1214
def test_get_mimetype_id_invalid(mimetype):
15+
logging.disable(logging.CRITICAL)
1316
assert not get_mimetype_id(mimetype)
17+
logging.disable(logging.NOTSET)
18+
19+
20+
def test_mimetype_other():
21+
for key, value in {
22+
"test_dir/empty_dir/empty_file.bin": "application/x-bin",
23+
"test_dir/hopper.png": "image/png",
24+
"test_dir/test.txt": "text/plain",
25+
}.items():
26+
assert fs_node_info(key, user_id="admin")["mimetype"] == get_mimetype_id(value)

tests/nc_py_api/occ_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ def test_get_cloud_app_config_invalid_name():
4343

4444

4545
def test_get_cloud_app_config_default_value():
46+
logging.disable(logging.CRITICAL)
4647
assert nc_py_api.get_cloud_app_config_value("core", "invalid_name", default=3) == 3
48+
logging.disable(logging.NOTSET)
4749

4850

4951
@mock.patch("nc_py_api.occ._PHP_PATH", "no_php")

tests/nc_py_api/test_dir/test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Test file

0 commit comments

Comments
 (0)