stabilize directory workflow
This commit is contained in:
parent
680b0172f0
commit
b3be3d25ee
@ -12,6 +12,7 @@ from materia.models.database import (
|
|||||||
DatabaseMigrationError,
|
DatabaseMigrationError,
|
||||||
Cache,
|
Cache,
|
||||||
CacheError,
|
CacheError,
|
||||||
|
SessionContext,
|
||||||
)
|
)
|
||||||
|
|
||||||
from materia.models.user import User, UserCredentials, UserInfo
|
from materia.models.user import User, UserCredentials, UserInfo
|
||||||
@ -27,9 +28,18 @@ from materia.models.repository import (
|
|||||||
|
|
||||||
from materia.models.directory import (
|
from materia.models.directory import (
|
||||||
Directory,
|
Directory,
|
||||||
DirectoryPath,
|
|
||||||
DirectoryLink,
|
DirectoryLink,
|
||||||
DirectoryInfo,
|
DirectoryInfo,
|
||||||
|
DirectoryPath,
|
||||||
|
DirectoryRename,
|
||||||
|
DirectoryCopyMove,
|
||||||
)
|
)
|
||||||
|
|
||||||
from materia.models.file import File, FileLink, FileInfo
|
from materia.models.file import (
|
||||||
|
File,
|
||||||
|
FileLink,
|
||||||
|
FileInfo,
|
||||||
|
FilePath,
|
||||||
|
FileRename,
|
||||||
|
FileCopyMove,
|
||||||
|
)
|
||||||
|
@ -107,7 +107,7 @@ class Database:
|
|||||||
raise e from None
|
raise e from None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
raise DatabaseError(*e.args) from e
|
raise e # DatabaseError(*e.args) from e
|
||||||
finally:
|
finally:
|
||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
|
@ -48,8 +48,8 @@ class Directory(Base):
|
|||||||
await session.flush()
|
await session.flush()
|
||||||
await session.refresh(self, attribute_names=["repository"])
|
await session.refresh(self, attribute_names=["repository"])
|
||||||
|
|
||||||
repository_path = await self.repository.path(session, config)
|
repository_path = await self.repository.real_path(session, config)
|
||||||
directory_path = await self.path(session, config)
|
directory_path = await self.real_path(session, config)
|
||||||
|
|
||||||
new_directory = FileSystem(directory_path, repository_path)
|
new_directory = FileSystem(directory_path, repository_path)
|
||||||
await new_directory.make_directory()
|
await new_directory.make_directory()
|
||||||
@ -70,8 +70,8 @@ class Directory(Base):
|
|||||||
for file in self.files:
|
for file in self.files:
|
||||||
file.remove(session, config)
|
file.remove(session, config)
|
||||||
|
|
||||||
repository_path = await self.repository.path(session, config)
|
repository_path = await self.repository.real_path(session, config)
|
||||||
directory_path = await self.path(session, config)
|
directory_path = await self.real_path(session, config)
|
||||||
|
|
||||||
current_directory = FileSystem(directory_path, repository_path)
|
current_directory = FileSystem(directory_path, repository_path)
|
||||||
await current_directory.remove()
|
await current_directory.remove()
|
||||||
@ -80,32 +80,45 @@ class Directory(Base):
|
|||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
||||||
async def relative_path(self, session: SessionContext) -> Optional[Path]:
|
async def relative_path(self, session: SessionContext) -> Optional[Path]:
|
||||||
"""Get relative path of the current directory"""
|
"""Get path of the directory relative repository root."""
|
||||||
if inspect(self).was_deleted:
|
if inspect(self).was_deleted:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
current_directory = self
|
current_directory = self
|
||||||
|
|
||||||
async with session.begin_nested():
|
|
||||||
while True:
|
while True:
|
||||||
|
# ISSUE: accessing `parent` attribute raises greenlet_spawn has not been called; can't call await_only() here
|
||||||
|
# parts.append(current_directory.name)
|
||||||
|
# session.add(current_directory)
|
||||||
|
# await session.refresh(current_directory, attribute_names=["parent"])
|
||||||
|
# if current_directory.parent is None:
|
||||||
|
# break
|
||||||
|
# current_directory = current_directory.parent
|
||||||
|
|
||||||
parts.append(current_directory.name)
|
parts.append(current_directory.name)
|
||||||
|
|
||||||
session.add(current_directory)
|
if current_directory.parent_id is None:
|
||||||
await session.refresh(current_directory, attribute_names=["parent"])
|
|
||||||
|
|
||||||
if current_directory.parent is None:
|
|
||||||
break
|
break
|
||||||
|
|
||||||
current_directory = current_directory.parent
|
current_directory = (
|
||||||
|
await session.scalars(
|
||||||
|
sa.select(Directory).where(
|
||||||
|
Directory.id == current_directory.parent_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
|
||||||
return Path().joinpath(*reversed(parts))
|
return Path().joinpath(*reversed(parts))
|
||||||
|
|
||||||
async def path(self, session: SessionContext, config: Config) -> Optional[Path]:
|
async def real_path(
|
||||||
|
self, session: SessionContext, config: Config
|
||||||
|
) -> Optional[Path]:
|
||||||
|
"""Get absolute path of the directory"""
|
||||||
if inspect(self).was_deleted:
|
if inspect(self).was_deleted:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
repository_path = await self.repository.path(session, config)
|
repository_path = await self.repository.real_path(session, config)
|
||||||
relative_path = await self.relative_path(session)
|
relative_path = await self.relative_path(session)
|
||||||
|
|
||||||
return repository_path.joinpath(relative_path)
|
return repository_path.joinpath(relative_path)
|
||||||
@ -123,6 +136,7 @@ class Directory(Base):
|
|||||||
current_directory: Optional[Directory] = None
|
current_directory: Optional[Directory] = None
|
||||||
|
|
||||||
for part in path.parts:
|
for part in path.parts:
|
||||||
|
# from root directory to target directory
|
||||||
current_directory = (
|
current_directory = (
|
||||||
await session.scalars(
|
await session.scalars(
|
||||||
sa.select(Directory).where(
|
sa.select(Directory).where(
|
||||||
@ -145,77 +159,108 @@ class Directory(Base):
|
|||||||
return current_directory
|
return current_directory
|
||||||
|
|
||||||
async def copy(
|
async def copy(
|
||||||
self, directory: Optional["Directory"], session: SessionContext, config: Config
|
self,
|
||||||
|
target: Optional["Directory"],
|
||||||
|
session: SessionContext,
|
||||||
|
config: Config,
|
||||||
|
force: bool = False,
|
||||||
|
shallow: bool = False,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
session.add(self)
|
session.add(self)
|
||||||
await session.refresh(self, attribute_names=["repository"])
|
await session.refresh(self, attribute_names=["repository"])
|
||||||
|
|
||||||
repository_path = await self.repository.path(session, config)
|
repository_path = await self.repository.real_path(session, config)
|
||||||
directory_path = await self.path(session, config)
|
directory_path = await self.real_path(session, config)
|
||||||
directory_path = (
|
target_path = (
|
||||||
await directory.path(session, config) if directory else repository_path
|
await target.real_path(session, config) if target else repository_path
|
||||||
)
|
)
|
||||||
|
|
||||||
current_directory = FileSystem(directory_path, repository_path)
|
current_directory = FileSystem(directory_path, repository_path)
|
||||||
new_directory = await current_directory.copy(directory_path)
|
new_directory = await current_directory.copy(
|
||||||
|
target_path, force=force, shallow=shallow
|
||||||
|
)
|
||||||
|
|
||||||
cloned = self.clone()
|
cloned = self.clone()
|
||||||
cloned.name = new_directory.name()
|
cloned.name = new_directory.name()
|
||||||
cloned.parent_id = directory.id if directory else None
|
cloned.parent_id = target.id if target else None
|
||||||
session.add(cloned)
|
session.add(cloned)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
||||||
|
await session.refresh(self, attribute_names=["files", "directories"])
|
||||||
|
for directory in self.directories:
|
||||||
|
await directory.copy(cloned, session, config, shallow=True)
|
||||||
|
for file in self.files:
|
||||||
|
await file.copy(cloned, session, config, shallow=True)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def move(
|
async def move(
|
||||||
self, directory: Optional["Directory"], session: SessionContext, config: Config
|
self,
|
||||||
|
target: Optional["Directory"],
|
||||||
|
session: SessionContext,
|
||||||
|
config: Config,
|
||||||
|
force: bool = False,
|
||||||
|
shallow: bool = False,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
session.add(self)
|
session.add(self)
|
||||||
await session.refresh(self, attribute_names=["repository"])
|
await session.refresh(self, attribute_names=["repository"])
|
||||||
|
|
||||||
repository_path = await self.repository.path(session, config)
|
repository_path = await self.repository.real_path(session, config)
|
||||||
directory_path = await self.path(session, config)
|
directory_path = await self.real_path(session, config)
|
||||||
directory_path = (
|
target_path = (
|
||||||
await directory.path(session, config) if directory else repository_path
|
await target.real_path(session, config) if target else repository_path
|
||||||
)
|
)
|
||||||
|
|
||||||
current_directory = FileSystem(directory_path, repository_path)
|
current_directory = FileSystem(directory_path, repository_path)
|
||||||
moved_directory = await current_directory.move(directory_path)
|
moved_directory = await current_directory.move(
|
||||||
|
target_path, force=force, shallow=shallow
|
||||||
|
)
|
||||||
|
|
||||||
self.name = moved_directory.name()
|
self.name = moved_directory.name()
|
||||||
self.parent_id = directory.id if directory else None
|
self.parent_id = target.id if target else None
|
||||||
self.updated = time()
|
self.updated = time()
|
||||||
|
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def rename(self, name: str, session: SessionContext, config: Config) -> Self:
|
async def rename(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
session: SessionContext,
|
||||||
|
config: Config,
|
||||||
|
force: bool = False,
|
||||||
|
shallow: bool = False,
|
||||||
|
) -> Self:
|
||||||
session.add(self)
|
session.add(self)
|
||||||
await session.refresh(self, attribute_names=["repository"])
|
await session.refresh(self, attribute_names=["repository"])
|
||||||
|
|
||||||
repository_path = await self.repository.path(session, config)
|
repository_path = await self.repository.real_path(session, config)
|
||||||
directory_path = await self.path(session, config)
|
directory_path = await self.real_path(session, config)
|
||||||
|
|
||||||
current_directory = FileSystem(directory_path, repository_path)
|
current_directory = FileSystem(directory_path, repository_path)
|
||||||
renamed_directory = await current_directory.rename(name, force=True)
|
renamed_directory = await current_directory.rename(
|
||||||
|
name, force=force, shallow=shallow
|
||||||
|
)
|
||||||
|
|
||||||
self.name = renamed_directory.name()
|
self.name = renamed_directory.name()
|
||||||
await session.flush()
|
await session.flush()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def info(self, session: SessionContext) -> "DirectoryInfo":
|
async def info(self, session: SessionContext) -> "DirectoryInfo":
|
||||||
info = DirectoryInfo.model_validate(self)
|
|
||||||
session.add(self)
|
session.add(self)
|
||||||
await session.refresh(self, attribute_names=["files"])
|
await session.refresh(self, attribute_names=["files"])
|
||||||
|
|
||||||
|
info = DirectoryInfo.model_validate(self)
|
||||||
|
|
||||||
|
relative_path = await self.relative_path(session)
|
||||||
|
|
||||||
|
info.path = Path("/").joinpath(relative_path) if relative_path else None
|
||||||
info.used = sum([file.size for file in self.files])
|
info.used = sum([file.size for file in self.files])
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
class DirectoryPath(BaseModel):
|
|
||||||
path: Path
|
|
||||||
|
|
||||||
|
|
||||||
class DirectoryLink(Base):
|
class DirectoryLink(Base):
|
||||||
__tablename__ = "directory_link"
|
__tablename__ = "directory_link"
|
||||||
|
|
||||||
@ -240,9 +285,26 @@ class DirectoryInfo(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
is_public: bool
|
is_public: bool
|
||||||
|
|
||||||
|
path: Optional[Path] = None
|
||||||
used: Optional[int] = None
|
used: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DirectoryPath(BaseModel):
|
||||||
|
path: Path
|
||||||
|
|
||||||
|
|
||||||
|
class DirectoryRename(BaseModel):
|
||||||
|
path: Path
|
||||||
|
name: str
|
||||||
|
force: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
|
class DirectoryCopyMove(BaseModel):
|
||||||
|
path: Path
|
||||||
|
target: Path
|
||||||
|
force: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
from materia.models.repository import Repository
|
from materia.models.repository import Repository
|
||||||
from materia.models.file import File
|
from materia.models.file import File
|
||||||
from materia.models.filesystem import FileSystem
|
from materia.models.filesystem import FileSystem
|
||||||
|
@ -47,9 +47,11 @@ class File(Base):
|
|||||||
await session.flush()
|
await session.flush()
|
||||||
await session.refresh(self, attribute_names=["repository"])
|
await session.refresh(self, attribute_names=["repository"])
|
||||||
|
|
||||||
file_path = await self.path(session, config)
|
file_path = await self.real_path(session, config)
|
||||||
|
|
||||||
new_file = FileSystem(file_path, await self.repository.path(session, config))
|
new_file = FileSystem(
|
||||||
|
file_path, await self.repository.real_path(session, config)
|
||||||
|
)
|
||||||
await new_file.write_file(data)
|
await new_file.write_file(data)
|
||||||
|
|
||||||
self.size = await new_file.size()
|
self.size = await new_file.size()
|
||||||
@ -60,9 +62,11 @@ class File(Base):
|
|||||||
async def remove(self, session: SessionContext, config: Config):
|
async def remove(self, session: SessionContext, config: Config):
|
||||||
session.add(self)
|
session.add(self)
|
||||||
|
|
||||||
file_path = await self.path(session, config)
|
file_path = await self.real_path(session, config)
|
||||||
|
|
||||||
new_file = FileSystem(file_path, await self.repository.path(session, config))
|
new_file = FileSystem(
|
||||||
|
file_path, await self.repository.real_path(session, config)
|
||||||
|
)
|
||||||
await new_file.remove()
|
await new_file.remove()
|
||||||
|
|
||||||
await session.delete(self)
|
await session.delete(self)
|
||||||
@ -83,7 +87,9 @@ class File(Base):
|
|||||||
|
|
||||||
return file_path.joinpath(self.name)
|
return file_path.joinpath(self.name)
|
||||||
|
|
||||||
async def path(self, session: SessionContext, config: Config) -> Optional[Path]:
|
async def real_path(
|
||||||
|
self, session: SessionContext, config: Config
|
||||||
|
) -> Optional[Path]:
|
||||||
if inspect(self).was_deleted:
|
if inspect(self).was_deleted:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -94,9 +100,9 @@ class File(Base):
|
|||||||
await session.refresh(self, attribute_names=["repository", "parent"])
|
await session.refresh(self, attribute_names=["repository", "parent"])
|
||||||
|
|
||||||
if self.parent:
|
if self.parent:
|
||||||
file_path = await self.parent.path(session, config)
|
file_path = await self.parent.real_path(session, config)
|
||||||
else:
|
else:
|
||||||
file_path = await self.repository.path(session, config)
|
file_path = await self.repository.real_path(session, config)
|
||||||
|
|
||||||
return file_path.joinpath(self.name)
|
return file_path.joinpath(self.name)
|
||||||
|
|
||||||
@ -130,19 +136,24 @@ class File(Base):
|
|||||||
return current_file
|
return current_file
|
||||||
|
|
||||||
async def copy(
|
async def copy(
|
||||||
self, directory: Optional["Directory"], session: SessionContext, config: Config
|
self,
|
||||||
|
directory: Optional["Directory"],
|
||||||
|
session: SessionContext,
|
||||||
|
config: Config,
|
||||||
|
force: bool = False,
|
||||||
|
shallow: bool = False,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
session.add(self)
|
session.add(self)
|
||||||
await session.refresh(self, attribute_names=["repository"])
|
await session.refresh(self, attribute_names=["repository"])
|
||||||
|
|
||||||
repository_path = await self.repository.path(session, config)
|
repository_path = await self.repository.real_path(session, config)
|
||||||
file_path = await self.path(session, config)
|
file_path = await self.real_path(session, config)
|
||||||
directory_path = (
|
directory_path = (
|
||||||
await directory.path(session, config) if directory else repository_path
|
await directory.real_path(session, config) if directory else repository_path
|
||||||
)
|
)
|
||||||
|
|
||||||
current_file = FileSystem(file_path, repository_path)
|
current_file = FileSystem(file_path, repository_path)
|
||||||
new_file = await current_file.copy(directory_path)
|
new_file = await current_file.copy(directory_path, force=force, shallow=shallow)
|
||||||
|
|
||||||
cloned = self.clone()
|
cloned = self.clone()
|
||||||
cloned.name = new_file.name()
|
cloned.name = new_file.name()
|
||||||
@ -153,19 +164,26 @@ class File(Base):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
async def move(
|
async def move(
|
||||||
self, directory: Optional["Directory"], session: SessionContext, config: Config
|
self,
|
||||||
|
directory: Optional["Directory"],
|
||||||
|
session: SessionContext,
|
||||||
|
config: Config,
|
||||||
|
force: bool = False,
|
||||||
|
shallow: bool = False,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
session.add(self)
|
session.add(self)
|
||||||
await session.refresh(self, attribute_names=["repository"])
|
await session.refresh(self, attribute_names=["repository"])
|
||||||
|
|
||||||
repository_path = await self.repository.path(session, config)
|
repository_path = await self.repository.real_path(session, config)
|
||||||
file_path = await self.path(session, config)
|
file_path = await self.real_path(session, config)
|
||||||
directory_path = (
|
directory_path = (
|
||||||
await directory.path(session, config) if directory else repository_path
|
await directory.real_path(session, config) if directory else repository_path
|
||||||
)
|
)
|
||||||
|
|
||||||
current_file = FileSystem(file_path, repository_path)
|
current_file = FileSystem(file_path, repository_path)
|
||||||
moved_file = await current_file.move(directory_path)
|
moved_file = await current_file.move(
|
||||||
|
directory_path, force=force, shallow=shallow
|
||||||
|
)
|
||||||
|
|
||||||
self.name = moved_file.name()
|
self.name = moved_file.name()
|
||||||
self.parent_id = directory.id if directory else None
|
self.parent_id = directory.id if directory else None
|
||||||
@ -174,15 +192,22 @@ class File(Base):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def rename(self, name: str, session: SessionContext, config: Config) -> Self:
|
async def rename(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
session: SessionContext,
|
||||||
|
config: Config,
|
||||||
|
force: bool = False,
|
||||||
|
shallow: bool = False,
|
||||||
|
) -> Self:
|
||||||
session.add(self)
|
session.add(self)
|
||||||
await session.refresh(self, attribute_names=["repository"])
|
await session.refresh(self, attribute_names=["repository"])
|
||||||
|
|
||||||
repository_path = await self.repository.path(session, config)
|
repository_path = await self.repository.real_path(session, config)
|
||||||
file_path = await self.path(session, config)
|
file_path = await self.real_path(session, config)
|
||||||
|
|
||||||
current_file = FileSystem(file_path, repository_path)
|
current_file = FileSystem(file_path, repository_path)
|
||||||
renamed_file = await current_file.rename(name, force=True)
|
renamed_file = await current_file.rename(name, force=force, shallow=shallow)
|
||||||
|
|
||||||
self.name = renamed_file.name()
|
self.name = renamed_file.name()
|
||||||
self.updated = time()
|
self.updated = time()
|
||||||
@ -226,6 +251,22 @@ class FileInfo(BaseModel):
|
|||||||
size: int
|
size: int
|
||||||
|
|
||||||
|
|
||||||
|
class FilePath(BaseModel):
|
||||||
|
path: Path
|
||||||
|
|
||||||
|
|
||||||
|
class FileRename(BaseModel):
|
||||||
|
path: Path
|
||||||
|
name: str
|
||||||
|
force: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
|
class FileCopyMove(BaseModel):
|
||||||
|
path: Path
|
||||||
|
target: Path
|
||||||
|
force: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
from materia.models.repository import Repository
|
from materia.models.repository import Repository
|
||||||
from materia.models.directory import Directory
|
from materia.models.directory import Directory
|
||||||
from materia.models.filesystem import FileSystem
|
from materia.models.filesystem import FileSystem
|
||||||
|
@ -59,7 +59,7 @@ class FileSystem:
|
|||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise FileSystemError(
|
raise FileSystemError(
|
||||||
f"Failed to remove file system content at /{self.relative_path}:",
|
f"Failed to remove content at /{self.relative_path}:",
|
||||||
*e.args,
|
*e.args,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -68,9 +68,6 @@ class FileSystem:
|
|||||||
count = 1
|
count = 1
|
||||||
new_path = target_directory.joinpath(name)
|
new_path = target_directory.joinpath(name)
|
||||||
|
|
||||||
if new_path == self.path:
|
|
||||||
return name
|
|
||||||
|
|
||||||
while await async_path.exists(new_path):
|
while await async_path.exists(new_path):
|
||||||
if await self.is_file():
|
if await self.is_file():
|
||||||
if with_counter := re.match(r"^(.+)\.(\d+)\.(\w+)$", new_path.name):
|
if with_counter := re.match(r"^(.+)\.(\d+)\.(\w+)$", new_path.name):
|
||||||
@ -94,26 +91,41 @@ class FileSystem:
|
|||||||
|
|
||||||
return new_path.name
|
return new_path.name
|
||||||
|
|
||||||
|
async def _generate_new_path(
|
||||||
|
self,
|
||||||
|
target_directory: Path,
|
||||||
|
new_name: Optional[str] = None,
|
||||||
|
force: bool = False,
|
||||||
|
shallow: bool = False,
|
||||||
|
) -> Path:
|
||||||
|
if self.path == self.working_directory:
|
||||||
|
raise FileSystemError("Cannot modify working directory")
|
||||||
|
|
||||||
|
new_name = new_name or self.path.name
|
||||||
|
|
||||||
|
if await async_path.exists(target_directory.joinpath(new_name)) and not shallow:
|
||||||
|
if force:
|
||||||
|
new_name = await self.generate_name(target_directory, new_name)
|
||||||
|
else:
|
||||||
|
raise FileSystemError(
|
||||||
|
f"Target destination already exists /{target_directory.joinpath(new_name)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return target_directory.joinpath(new_name)
|
||||||
|
|
||||||
async def move(
|
async def move(
|
||||||
self,
|
self,
|
||||||
target_directory: Path,
|
target_directory: Path,
|
||||||
new_name: Optional[str] = None,
|
new_name: Optional[str] = None,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
|
shallow: bool = False,
|
||||||
):
|
):
|
||||||
new_name = new_name if new_name else self.path.name
|
new_path = await self._generate_new_path(
|
||||||
|
target_directory, new_name, force=force, shallow=shallow
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if (
|
if not shallow:
|
||||||
await async_path.exists(target_directory.joinpath(new_name))
|
|
||||||
and not force
|
|
||||||
):
|
|
||||||
raise FileSystemError(
|
|
||||||
"Failed to move content to target destination: already exists"
|
|
||||||
)
|
|
||||||
|
|
||||||
new_path = target_directory.joinpath(
|
|
||||||
await self.generate_name(target_directory, new_name)
|
|
||||||
)
|
|
||||||
await aioshutil.move(self.path, new_path)
|
await aioshutil.move(self.path, new_path)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -124,30 +136,26 @@ class FileSystem:
|
|||||||
|
|
||||||
return FileSystem(new_path, self.working_directory)
|
return FileSystem(new_path, self.working_directory)
|
||||||
|
|
||||||
async def rename(self, new_name: str, force: bool = False) -> Path:
|
async def rename(
|
||||||
return await self.move(self.path.parent, new_name=new_name, force=force)
|
self, new_name: str, force: bool = False, shallow: bool = False
|
||||||
|
) -> Path:
|
||||||
|
return await self.move(
|
||||||
|
self.path.parent, new_name=new_name, force=force, shallow=shallow
|
||||||
|
)
|
||||||
|
|
||||||
async def copy(
|
async def copy(
|
||||||
self,
|
self,
|
||||||
target_directory: Path,
|
target_directory: Path,
|
||||||
new_name: Optional[str] = None,
|
new_name: Optional[str] = None,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
|
shallow: bool = False,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
new_name = new_name if new_name else self.path.name
|
new_path = await self._generate_new_path(
|
||||||
|
target_directory, new_name, force=force, shallow=shallow
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if (
|
if not shallow:
|
||||||
await async_path.exists(target_directory.joinpath(new_name))
|
|
||||||
and not force
|
|
||||||
):
|
|
||||||
raise FileSystemError(
|
|
||||||
"Failed to copy content to target destination: already exists"
|
|
||||||
)
|
|
||||||
|
|
||||||
new_path = target_directory.joinpath(
|
|
||||||
await self.generate_name(target_directory, new_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
if await self.is_file():
|
if await self.is_file():
|
||||||
await aioshutil.copy(self.path, new_path)
|
await aioshutil.copy(self.path, new_path)
|
||||||
|
|
||||||
@ -156,7 +164,7 @@ class FileSystem:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise FileSystemError(
|
raise FileSystemError(
|
||||||
f"Failed to copy content from /{self.relative_path}:",
|
f"Failed to copy content from /{new_path}:",
|
||||||
*e.args,
|
*e.args,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -193,6 +201,7 @@ class FileSystem:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def normalize(path: Path) -> Path:
|
def normalize(path: Path) -> Path:
|
||||||
|
"""Resolve path and make it relative."""
|
||||||
if not path.is_absolute():
|
if not path.is_absolute():
|
||||||
path = Path("/").joinpath(path)
|
path = Path("/").joinpath(path)
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ class Repository(Base):
|
|||||||
session.add(self)
|
session.add(self)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
||||||
repository_path = await self.path(session, config)
|
repository_path = await self.real_path(session, config)
|
||||||
relative_path = repository_path.relative_to(
|
relative_path = repository_path.relative_to(
|
||||||
config.application.working_directory
|
config.application.working_directory
|
||||||
)
|
)
|
||||||
@ -52,12 +52,13 @@ class Repository(Base):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def path(self, session: SessionContext, config: Config) -> Path:
|
async def real_path(self, session: SessionContext, config: Config) -> Path:
|
||||||
|
"""Get absolute path of the directory."""
|
||||||
session.add(self)
|
session.add(self)
|
||||||
await session.refresh(self, attribute_names=["user"])
|
await session.refresh(self, attribute_names=["user"])
|
||||||
|
|
||||||
repository_path = config.application.working_directory.joinpath(
|
repository_path = config.application.working_directory.joinpath(
|
||||||
"repository", self.user.lower_name, "default"
|
"repository", self.user.lower_name
|
||||||
)
|
)
|
||||||
|
|
||||||
return repository_path
|
return repository_path
|
||||||
@ -73,7 +74,7 @@ class Repository(Base):
|
|||||||
for file in self.files:
|
for file in self.files:
|
||||||
await file.remove(session)
|
await file.remove(session)
|
||||||
|
|
||||||
repository_path = await self.path(session, config)
|
repository_path = await self.real_path(session, config)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(str(repository_path))
|
shutil.rmtree(str(repository_path))
|
||||||
|
@ -4,7 +4,17 @@ import shutil
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
|
||||||
from materia.models import User, Directory, DirectoryPath, DirectoryInfo, FileSystem
|
from materia.models import (
|
||||||
|
User,
|
||||||
|
Directory,
|
||||||
|
DirectoryInfo,
|
||||||
|
DirectoryPath,
|
||||||
|
DirectoryRename,
|
||||||
|
DirectoryCopyMove,
|
||||||
|
FileSystem,
|
||||||
|
Repository,
|
||||||
|
)
|
||||||
|
from materia.models.database import SessionContext
|
||||||
from materia.routers import middleware
|
from materia.routers import middleware
|
||||||
from materia.config import Config
|
from materia.config import Config
|
||||||
|
|
||||||
@ -13,23 +23,65 @@ from pydantic import BaseModel
|
|||||||
router = APIRouter(tags=["directory"])
|
router = APIRouter(tags=["directory"])
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_current_directory(
|
||||||
|
path: Path, repository: Repository, session: SessionContext, config: Config
|
||||||
|
) -> Directory:
|
||||||
|
if not FileSystem.check_path(path):
|
||||||
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid path")
|
||||||
|
|
||||||
|
if not (
|
||||||
|
directory := await Directory.by_path(
|
||||||
|
repository,
|
||||||
|
FileSystem.normalize(path),
|
||||||
|
session,
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory not found")
|
||||||
|
|
||||||
|
return directory
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_target_directory(
|
||||||
|
path: Path, repository: Repository, session: SessionContext, config: Config
|
||||||
|
) -> Directory:
|
||||||
|
if not FileSystem.check_path(path):
|
||||||
|
raise HTTPException(
|
||||||
|
status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid target path"
|
||||||
|
)
|
||||||
|
|
||||||
|
if FileSystem.normalize(path) == Path():
|
||||||
|
# mean repository root
|
||||||
|
target_directory = None
|
||||||
|
else:
|
||||||
|
if not (
|
||||||
|
target_directory := await Directory.by_path(
|
||||||
|
repository,
|
||||||
|
FileSystem.normalize(path),
|
||||||
|
session,
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "Target directory not found")
|
||||||
|
|
||||||
|
return target_directory
|
||||||
|
|
||||||
|
|
||||||
@router.post("/directory")
|
@router.post("/directory")
|
||||||
async def create(
|
async def create(
|
||||||
path: DirectoryPath,
|
path: DirectoryPath,
|
||||||
repository=Depends(middleware.repository),
|
repository: Repository = Depends(middleware.repository),
|
||||||
ctx: middleware.Context = Depends(),
|
ctx: middleware.Context = Depends(),
|
||||||
):
|
):
|
||||||
if not FileSystem.check_path(path.path):
|
if not FileSystem.check_path(path.path):
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid path")
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid path")
|
||||||
|
|
||||||
path = FileSystem.normalize(path.path)
|
|
||||||
|
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
current_directory = None
|
current_directory = None
|
||||||
current_path = Path()
|
current_path = Path()
|
||||||
directory = None
|
directory = None
|
||||||
|
|
||||||
for part in path.parts:
|
for part in FileSystem.normalize(path.path).parts:
|
||||||
if not (
|
if not (
|
||||||
directory := await Directory.by_path(
|
directory := await Directory.by_path(
|
||||||
repository, current_path.joinpath(part), session, ctx.config
|
repository, current_path.joinpath(part), session, ctx.config
|
||||||
@ -50,65 +102,80 @@ async def create(
|
|||||||
@router.get("/directory")
|
@router.get("/directory")
|
||||||
async def info(
|
async def info(
|
||||||
path: Path,
|
path: Path,
|
||||||
repository=Depends(middleware.repository),
|
repository: Repository = Depends(middleware.repository),
|
||||||
ctx: middleware.Context = Depends(),
|
ctx: middleware.Context = Depends(),
|
||||||
):
|
):
|
||||||
if not FileSystem.check_path(path):
|
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid path")
|
|
||||||
|
|
||||||
path = FileSystem.normalize(path)
|
|
||||||
ctx.logger.info(path)
|
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
if not (
|
directory = await validate_current_directory(
|
||||||
directory := await Directory.by_path(
|
path, repository, session, ctx.config
|
||||||
repository,
|
|
||||||
path,
|
|
||||||
session,
|
|
||||||
ctx.config,
|
|
||||||
)
|
)
|
||||||
):
|
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory not found")
|
|
||||||
ctx.logger.info(directory)
|
|
||||||
info = await directory.info(session)
|
info = await directory.info(session)
|
||||||
ctx.logger.info(info)
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/directory")
|
@router.delete("/directory")
|
||||||
async def remove(
|
async def remove(
|
||||||
path: Path,
|
path: Path,
|
||||||
repository=Depends(middleware.repository),
|
repository: Repository = Depends(middleware.repository),
|
||||||
ctx: middleware.Context = Depends(),
|
ctx: middleware.Context = Depends(),
|
||||||
):
|
):
|
||||||
if not FileSystem.check_path(path):
|
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid path")
|
|
||||||
|
|
||||||
path = FileSystem.normalize(path)
|
|
||||||
|
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
if not (
|
directory = await validate_current_directory(
|
||||||
directory := await Directory.by_path(
|
path, repository, session, ctx.config
|
||||||
repository,
|
|
||||||
path,
|
|
||||||
session,
|
|
||||||
ctx.config,
|
|
||||||
)
|
)
|
||||||
):
|
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory not found")
|
|
||||||
|
|
||||||
await directory.remove(session, ctx.config)
|
await directory.remove(session, ctx.config)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/directory/rename")
|
@router.patch("/directory/rename")
|
||||||
async def rename():
|
async def rename(
|
||||||
pass
|
data: DirectoryRename,
|
||||||
|
repository: Repository = Depends(middleware.repository),
|
||||||
|
ctx: middleware.Context = Depends(),
|
||||||
|
):
|
||||||
|
async with ctx.database.session() as session:
|
||||||
|
directory = await validate_current_directory(
|
||||||
|
data.path, repository, session, ctx.config
|
||||||
|
)
|
||||||
|
|
||||||
|
await directory.rename(data.name, session, ctx.config, force=data.force)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/directory/move")
|
@router.patch("/directory/move")
|
||||||
async def move():
|
async def move(
|
||||||
pass
|
data: DirectoryCopyMove,
|
||||||
|
repository: Repository = Depends(middleware.repository),
|
||||||
|
ctx: middleware.Context = Depends(),
|
||||||
|
):
|
||||||
|
async with ctx.database.session() as session:
|
||||||
|
directory = await validate_current_directory(
|
||||||
|
data.path, repository, session, ctx.config
|
||||||
|
)
|
||||||
|
target_directory = await validate_target_directory(
|
||||||
|
data.target, repository, session, ctx.config
|
||||||
|
)
|
||||||
|
|
||||||
|
await directory.move(target_directory, session, ctx.config, force=data.force)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/directory/copy")
|
@router.post("/directory/copy")
|
||||||
async def copy():
|
async def copy(
|
||||||
pass
|
data: DirectoryCopyMove,
|
||||||
|
repository: Repository = Depends(middleware.repository),
|
||||||
|
ctx: middleware.Context = Depends(),
|
||||||
|
):
|
||||||
|
async with ctx.database.session() as session:
|
||||||
|
directory = await validate_current_directory(
|
||||||
|
data.path, repository, session, ctx.config
|
||||||
|
)
|
||||||
|
target_directory = await validate_target_directory(
|
||||||
|
data.target, repository, session, ctx.config
|
||||||
|
)
|
||||||
|
|
||||||
|
await directory.copy(target_directory, session, ctx.config, force=data.force)
|
||||||
|
await session.commit()
|
||||||
|
@ -3,103 +3,84 @@ from pathlib import Path
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile
|
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile
|
||||||
|
|
||||||
from materia.models import User, File, FileInfo, Directory
|
from materia.models import (
|
||||||
|
User,
|
||||||
|
File,
|
||||||
|
FileInfo,
|
||||||
|
Directory,
|
||||||
|
DirectoryPath,
|
||||||
|
Repository,
|
||||||
|
FileSystem,
|
||||||
|
FileRename,
|
||||||
|
FilePath,
|
||||||
|
FileCopyMove,
|
||||||
|
)
|
||||||
|
from materia.models.database import SessionContext
|
||||||
from materia.routers import middleware
|
from materia.routers import middleware
|
||||||
from materia.config import Config
|
from materia.config import Config
|
||||||
|
from materia.routers.api.directory import validate_target_directory
|
||||||
|
|
||||||
router = APIRouter(tags=["file"])
|
router = APIRouter(tags=["file"])
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_current_file(
|
||||||
|
path: Path, repository: Repository, session: SessionContext, config: Config
|
||||||
|
) -> Directory:
|
||||||
|
if not FileSystem.check_path(path):
|
||||||
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid path")
|
||||||
|
|
||||||
|
if not (
|
||||||
|
file := await File.by_path(
|
||||||
|
repository,
|
||||||
|
FileSystem.normalize(path),
|
||||||
|
session,
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "File not found")
|
||||||
|
|
||||||
|
return file
|
||||||
|
|
||||||
|
|
||||||
@router.post("/file")
|
@router.post("/file")
|
||||||
async def create(
|
async def create(
|
||||||
upload_file: UploadFile,
|
file: UploadFile,
|
||||||
path: Path = Path(),
|
path: DirectoryPath,
|
||||||
user: User = Depends(middleware.user),
|
repository: Repository = Depends(middleware.repository),
|
||||||
ctx: middleware.Context = Depends(),
|
ctx: middleware.Context = Depends(),
|
||||||
):
|
):
|
||||||
if not upload_file.filename:
|
if not file.filename:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status.HTTP_417_EXPECTATION_FAILED, "Cannot upload file without name"
|
status.HTTP_417_EXPECTATION_FAILED, "Cannot upload file without name"
|
||||||
)
|
)
|
||||||
|
if not FileSystem.check_path(path.path):
|
||||||
repository_path = Config.data_dir() / "repository" / user.lower_name
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid path")
|
||||||
blacklist = [os.sep, ".", "..", "*"]
|
|
||||||
directory_path = Path(
|
|
||||||
os.sep.join(filter(lambda part: part not in blacklist, path.parts))
|
|
||||||
)
|
|
||||||
|
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
session.add(user)
|
target_directory = await validate_target_directory(
|
||||||
await session.refresh(user, attribute_names=["repository"])
|
path.path, repository, session, ctx.config
|
||||||
|
|
||||||
if not user.repository:
|
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
|
|
||||||
|
|
||||||
if not directory_path == Path():
|
|
||||||
directory = await Directory.by_path(
|
|
||||||
user.repository.id,
|
|
||||||
None if directory_path.parent == Path() else directory_path.parent,
|
|
||||||
directory_path.name,
|
|
||||||
ctx.database,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not directory:
|
await File(
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Directory not found")
|
repository_id=repository.id,
|
||||||
else:
|
parent_id=target_directory.id if target_directory else None,
|
||||||
directory = None
|
name=file.filename,
|
||||||
|
size=file.size,
|
||||||
|
).new(await file.read(), session, ctx.config)
|
||||||
|
|
||||||
file = File(
|
|
||||||
repository_id=user.repository.id,
|
|
||||||
parent_id=directory.id if directory else None,
|
|
||||||
name=upload_file.filename,
|
|
||||||
path=None if directory_path == Path() else str(directory_path),
|
|
||||||
size=upload_file.size,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
file_path = repository_path.joinpath(directory_path, upload_file.filename)
|
|
||||||
|
|
||||||
if file_path.exists():
|
|
||||||
raise HTTPException(
|
|
||||||
status.HTTP_409_CONFLICT, "File with given name already exists"
|
|
||||||
)
|
|
||||||
|
|
||||||
file_path.write_bytes(await upload_file.read())
|
|
||||||
except OSError:
|
|
||||||
raise HTTPException(
|
|
||||||
status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to write a file"
|
|
||||||
)
|
|
||||||
|
|
||||||
async with ctx.database.session() as session:
|
|
||||||
session.add(file)
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/file")
|
@router.get("/file")
|
||||||
async def info(
|
async def info(
|
||||||
path: Path,
|
path: Path,
|
||||||
user: User = Depends(middleware.user),
|
repository: Repository = Depends(middleware.repository),
|
||||||
ctx: middleware.Context = Depends(),
|
ctx: middleware.Context = Depends(),
|
||||||
):
|
):
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
session.add(user)
|
file = await validate_current_file(path, repository, session, ctx.config)
|
||||||
await session.refresh(user, attribute_names=["repository"])
|
|
||||||
|
|
||||||
if not user.repository:
|
info = await file.info(session)
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
|
|
||||||
|
|
||||||
if not (
|
|
||||||
file := await File.by_path(
|
|
||||||
user.repository.id,
|
|
||||||
None if path.parent == Path() else path.parent,
|
|
||||||
path.name,
|
|
||||||
ctx.database,
|
|
||||||
)
|
|
||||||
):
|
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "File not found")
|
|
||||||
|
|
||||||
info = FileInfo.model_validate(file)
|
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
@ -107,36 +88,56 @@ async def info(
|
|||||||
@router.delete("/file")
|
@router.delete("/file")
|
||||||
async def remove(
|
async def remove(
|
||||||
path: Path,
|
path: Path,
|
||||||
user: User = Depends(middleware.user),
|
repository: Repository = Depends(middleware.repository),
|
||||||
ctx: middleware.Context = Depends(),
|
ctx: middleware.Context = Depends(),
|
||||||
):
|
):
|
||||||
async with ctx.database.session() as session:
|
async with ctx.database.session() as session:
|
||||||
session.add(user)
|
file = await validate_current_file(path, repository, session, ctx.config)
|
||||||
await session.refresh(user, attribute_names=["repository"])
|
|
||||||
|
|
||||||
if not user.repository:
|
await file.remove(session, ctx.config)
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Repository not found")
|
await session.commit()
|
||||||
|
|
||||||
if not (
|
|
||||||
file := await File.by_path(
|
@router.patch("/file/rename")
|
||||||
user.repository.id,
|
async def rename(
|
||||||
None if path.parent == Path() else path.parent,
|
data: FileRename,
|
||||||
path.name,
|
repository: Repository = Depends(middleware.repository),
|
||||||
ctx.database,
|
ctx: middleware.Context = Depends(),
|
||||||
)
|
|
||||||
):
|
):
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "File not found")
|
async with ctx.database.session() as session:
|
||||||
|
file = await validate_current_file(data.path, repository, session, ctx.config)
|
||||||
|
|
||||||
repository_path = Config.data_dir() / "repository" / user.lower_name
|
await file.rename(data.name, session, ctx.config, force=data.force)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
try:
|
|
||||||
file_path = repository_path.joinpath(path)
|
|
||||||
|
|
||||||
if file_path.exists():
|
@router.patch("/file/move")
|
||||||
file_path.unlink(missing_ok=True)
|
async def move(
|
||||||
except OSError:
|
data: FileCopyMove,
|
||||||
raise HTTPException(
|
repository: Repository = Depends(middleware.repository),
|
||||||
status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to remove a file"
|
ctx: middleware.Context = Depends(),
|
||||||
|
):
|
||||||
|
async with ctx.database.session() as session:
|
||||||
|
file = await validate_current_file(data.path, repository, session, ctx.config)
|
||||||
|
target_directory = await validate_target_directory(
|
||||||
|
data.target, repository, session, ctx.config
|
||||||
)
|
)
|
||||||
|
|
||||||
await file.remove(ctx.database)
|
await file.move(target_directory, session, ctx.config, force=data.force)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/file/copy")
|
||||||
|
async def copy(
|
||||||
|
data: FileCopyMove,
|
||||||
|
repository: Repository = Depends(middleware.repository),
|
||||||
|
ctx: middleware.Context = Depends(),
|
||||||
|
):
|
||||||
|
async with ctx.database.session() as session:
|
||||||
|
file = await validate_current_file(data.path, repository, session, ctx.config)
|
||||||
|
target_directory = await validate_target_directory(
|
||||||
|
data.target, repository, session, ctx.config
|
||||||
|
)
|
||||||
|
|
||||||
|
await file.copy(target_directory, session, ctx.config, force=data.force)
|
||||||
|
await session.commit()
|
||||||
|
@ -21,6 +21,7 @@ from asgi_lifespan import LifespanManager
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from materia import routers
|
from materia import routers
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture(scope="session")
|
@pytest_asyncio.fixture(scope="session")
|
||||||
|
@ -5,6 +5,8 @@ from materia.models.base import Base
|
|||||||
import aiofiles
|
import aiofiles
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
# TODO: replace downloadable images for tests
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_auth(api_client: AsyncClient, api_config: Config):
|
async def test_auth(api_client: AsyncClient, api_config: Config):
|
||||||
@ -83,7 +85,7 @@ async def test_repository(auth_client: AsyncClient, api_config: Config):
|
|||||||
assert create.status_code == 409, create.text
|
assert create.status_code == 409, create.text
|
||||||
|
|
||||||
assert api_config.application.working_directory.joinpath(
|
assert api_config.application.working_directory.joinpath(
|
||||||
"repository", "PyTest".lower(), "default"
|
"repository", "PyTest".lower()
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
info = await auth_client.get("/api/repository")
|
info = await auth_client.get("/api/repository")
|
||||||
@ -100,6 +102,22 @@ async def test_repository(auth_client: AsyncClient, api_config: Config):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_directory(auth_client: AsyncClient, api_config: Config):
|
async def test_directory(auth_client: AsyncClient, api_config: Config):
|
||||||
|
first_dir_path = api_config.application.working_directory.joinpath(
|
||||||
|
"repository", "PyTest".lower(), "first_dir"
|
||||||
|
)
|
||||||
|
second_dir_path = api_config.application.working_directory.joinpath(
|
||||||
|
"repository", "PyTest".lower(), "second_dir"
|
||||||
|
)
|
||||||
|
second_dir_path_two = api_config.application.working_directory.joinpath(
|
||||||
|
"repository", "PyTest".lower(), "second_dir.1"
|
||||||
|
)
|
||||||
|
third_dir_path_one = api_config.application.working_directory.joinpath(
|
||||||
|
"repository", "PyTest".lower(), "third_dir"
|
||||||
|
)
|
||||||
|
third_dir_path_two = api_config.application.working_directory.joinpath(
|
||||||
|
"repository", "PyTest".lower(), "second_dir", "third_dir"
|
||||||
|
)
|
||||||
|
|
||||||
create = await auth_client.post("/api/repository")
|
create = await auth_client.post("/api/repository")
|
||||||
assert create.status_code == 200, create.text
|
assert create.status_code == 200, create.text
|
||||||
|
|
||||||
@ -109,17 +127,67 @@ async def test_directory(auth_client: AsyncClient, api_config: Config):
|
|||||||
create = await auth_client.post("/api/directory", json={"path": "/first_dir"})
|
create = await auth_client.post("/api/directory", json={"path": "/first_dir"})
|
||||||
assert create.status_code == 200, create.text
|
assert create.status_code == 200, create.text
|
||||||
|
|
||||||
assert api_config.application.working_directory.joinpath(
|
assert first_dir_path.exists()
|
||||||
"repository", "PyTest".lower(), "default", "first_dir"
|
|
||||||
).exists()
|
|
||||||
|
|
||||||
info = await auth_client.get("/api/directory", params=[("path", "/first_dir")])
|
info = await auth_client.get("/api/directory", params=[("path", "/first_dir")])
|
||||||
assert info.status_code == 200, info.text
|
assert info.status_code == 200, info.text
|
||||||
assert info.json()["used"] == 0
|
assert info.json()["used"] == 0
|
||||||
|
assert info.json()["path"] == "/first_dir"
|
||||||
|
|
||||||
delete = await auth_client.delete("/api/directory", params=[("path", "/first_dir")])
|
create = await auth_client.patch(
|
||||||
|
"/api/directory/rename",
|
||||||
|
json={"path": "/first_dir", "name": "first_dir_renamed"},
|
||||||
|
)
|
||||||
|
assert create.status_code == 200, create.text
|
||||||
|
|
||||||
|
delete = await auth_client.delete(
|
||||||
|
"/api/directory", params=[("path", "/first_dir_renamed")]
|
||||||
|
)
|
||||||
assert delete.status_code == 200, delete.text
|
assert delete.status_code == 200, delete.text
|
||||||
|
|
||||||
assert not api_config.application.working_directory.joinpath(
|
assert not first_dir_path.exists()
|
||||||
"repository", "PyTest".lower(), "default", "first_dir"
|
|
||||||
).exists()
|
create = await auth_client.post("/api/directory", json={"path": "/second_dir"})
|
||||||
|
assert create.status_code == 200, create.text
|
||||||
|
|
||||||
|
create = await auth_client.post("/api/directory", json={"path": "/third_dir"})
|
||||||
|
assert create.status_code == 200, create.text
|
||||||
|
|
||||||
|
move = await auth_client.patch(
|
||||||
|
"/api/directory/move", json={"path": "/third_dir", "target": "/second_dir"}
|
||||||
|
)
|
||||||
|
assert move.status_code == 200, move.text
|
||||||
|
assert not third_dir_path_one.exists()
|
||||||
|
assert third_dir_path_two.exists()
|
||||||
|
|
||||||
|
info = await auth_client.get(
|
||||||
|
"/api/directory", params=[("path", "/second_dir/third_dir")]
|
||||||
|
)
|
||||||
|
assert info.status_code == 200, info.text
|
||||||
|
assert info.json()["path"] == "/second_dir/third_dir"
|
||||||
|
|
||||||
|
copy = await auth_client.post(
|
||||||
|
"/api/directory/copy",
|
||||||
|
json={"path": "/second_dir", "target": "/", "force": True},
|
||||||
|
)
|
||||||
|
assert copy.status_code == 200, copy.text
|
||||||
|
assert second_dir_path.exists()
|
||||||
|
assert second_dir_path_two.exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_file(auth_client: AsyncClient, api_config: Config):
|
||||||
|
create = await auth_client.post("/api/repository")
|
||||||
|
assert create.status_code == 200, create.text
|
||||||
|
|
||||||
|
async with AsyncClient() as client:
|
||||||
|
pytest_logo_res = await client.get(
|
||||||
|
"https://docs.pytest.org/en/stable/_static/pytest1.png"
|
||||||
|
)
|
||||||
|
assert isinstance(pytest_logo_res.content, bytes)
|
||||||
|
pytest_logo = BytesIO(pytest_logo_res.content)
|
||||||
|
|
||||||
|
create = await auth_client.post(
|
||||||
|
"/api/file", files={"file": ("pytest.png", pytest_logo)}, json={"path", "/"}
|
||||||
|
)
|
||||||
|
assert create.status_code == 200, create.text
|
||||||
|
@ -60,7 +60,7 @@ async def test_repository(data, tmpdir, session: SessionContext, config: Config)
|
|||||||
|
|
||||||
assert repository
|
assert repository
|
||||||
assert repository.id is not None
|
assert repository.id is not None
|
||||||
assert (await repository.path(session, config)).exists()
|
assert (await repository.real_path(session, config)).exists()
|
||||||
assert await Repository.from_user(data.user, session) == repository
|
assert await Repository.from_user(data.user, session) == repository
|
||||||
|
|
||||||
await session.refresh(repository, attribute_names=["user"])
|
await session.refresh(repository, attribute_names=["user"])
|
||||||
@ -76,7 +76,7 @@ async def test_repository(data, tmpdir, session: SessionContext, config: Config)
|
|||||||
await session.flush()
|
await session.flush()
|
||||||
with pytest.raises(RepositoryError):
|
with pytest.raises(RepositoryError):
|
||||||
await repository.remove(session, config)
|
await repository.remove(session, config)
|
||||||
assert not (await repository.path(session, config)).exists()
|
assert not (await repository.real_path(session, config)).exists()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -107,7 +107,7 @@ async def test_directory(data, tmpdir, session: SessionContext, config: Config):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
).first() == directory
|
).first() == directory
|
||||||
assert (await directory.path(session, config)).exists()
|
assert (await directory.real_path(session, config)).exists()
|
||||||
|
|
||||||
# nested simple
|
# nested simple
|
||||||
nested_directory = await Directory(
|
nested_directory = await Directory(
|
||||||
@ -128,7 +128,7 @@ async def test_directory(data, tmpdir, session: SessionContext, config: Config):
|
|||||||
)
|
)
|
||||||
).first() == nested_directory
|
).first() == nested_directory
|
||||||
assert nested_directory.parent_id == directory.id
|
assert nested_directory.parent_id == directory.id
|
||||||
assert (await nested_directory.path(session, config)).exists()
|
assert (await nested_directory.real_path(session, config)).exists()
|
||||||
|
|
||||||
# relationship
|
# relationship
|
||||||
await session.refresh(directory, attribute_names=["directories", "files"])
|
await session.refresh(directory, attribute_names=["directories", "files"])
|
||||||
@ -150,30 +150,34 @@ async def test_directory(data, tmpdir, session: SessionContext, config: Config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# remove nested
|
# remove nested
|
||||||
nested_path = await nested_directory.path(session, config)
|
nested_path = await nested_directory.real_path(session, config)
|
||||||
assert nested_path.exists()
|
assert nested_path.exists()
|
||||||
await nested_directory.remove(session, config)
|
await nested_directory.remove(session, config)
|
||||||
assert inspect(nested_directory).was_deleted
|
assert inspect(nested_directory).was_deleted
|
||||||
assert await nested_directory.path(session, config) is None
|
assert await nested_directory.real_path(session, config) is None
|
||||||
assert not nested_path.exists()
|
assert not nested_path.exists()
|
||||||
|
|
||||||
await session.refresh(directory) # update attributes that was deleted
|
await session.refresh(directory) # update attributes that was deleted
|
||||||
assert (await directory.path(session, config)).exists()
|
assert (await directory.real_path(session, config)).exists()
|
||||||
|
|
||||||
# rename
|
# rename
|
||||||
assert (await directory.rename("test1", session, config)).name == "test1"
|
assert (
|
||||||
|
await directory.rename("test1", session, config, force=True)
|
||||||
|
).name == "test1.1"
|
||||||
await Directory(repository_id=repository.id, parent_id=None, name="test2").new(
|
await Directory(repository_id=repository.id, parent_id=None, name="test2").new(
|
||||||
session, config
|
session, config
|
||||||
)
|
)
|
||||||
assert (await directory.rename("test2", session, config)).name == "test2.1"
|
assert (
|
||||||
assert (await repository.path(session, config)).joinpath("test2.1").exists()
|
await directory.rename("test2", session, config, force=True)
|
||||||
assert not (await repository.path(session, config)).joinpath("test1").exists()
|
).name == "test2.1"
|
||||||
|
assert (await repository.real_path(session, config)).joinpath("test2.1").exists()
|
||||||
|
assert not (await repository.real_path(session, config)).joinpath("test1").exists()
|
||||||
|
|
||||||
directory_path = await directory.path(session, config)
|
directory_path = await directory.real_path(session, config)
|
||||||
assert directory_path.exists()
|
assert directory_path.exists()
|
||||||
|
|
||||||
await directory.remove(session, config)
|
await directory.remove(session, config)
|
||||||
assert await directory.path(session, config) is None
|
assert await directory.real_path(session, config) is None
|
||||||
assert not directory_path.exists()
|
assert not directory_path.exists()
|
||||||
|
|
||||||
|
|
||||||
@ -229,7 +233,7 @@ async def test_file(data, tmpdir, session: SessionContext, config: Config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
#
|
#
|
||||||
file_path = await file.path(session, config)
|
file_path = await file.real_path(session, config)
|
||||||
assert file_path.exists()
|
assert file_path.exists()
|
||||||
assert (await aiofiles.os.stat(file_path)).st_size == file.size
|
assert (await aiofiles.os.stat(file_path)).st_size == file.size
|
||||||
async with aiofiles.open(file_path, mode="rb") as io:
|
async with aiofiles.open(file_path, mode="rb") as io:
|
||||||
@ -238,21 +242,21 @@ async def test_file(data, tmpdir, session: SessionContext, config: Config):
|
|||||||
|
|
||||||
# rename
|
# rename
|
||||||
assert (
|
assert (
|
||||||
await file.rename("test_file_rename.txt", session, config)
|
await file.rename("test_file_rename.txt", session, config, force=True)
|
||||||
).name == "test_file_rename.txt"
|
).name == "test_file_rename.txt"
|
||||||
await File(
|
await File(
|
||||||
repository_id=repository.id, parent_id=directory.id, name="test_file_2.txt"
|
repository_id=repository.id, parent_id=directory.id, name="test_file_2.txt"
|
||||||
).new(b"", session, config)
|
).new(b"", session, config)
|
||||||
assert (
|
assert (
|
||||||
await file.rename("test_file_2.txt", session, config)
|
await file.rename("test_file_2.txt", session, config, force=True)
|
||||||
).name == "test_file_2.1.txt"
|
).name == "test_file_2.1.txt"
|
||||||
assert (
|
assert (
|
||||||
(await repository.path(session, config))
|
(await repository.real_path(session, config))
|
||||||
.joinpath("test1", "test_file_2.1.txt")
|
.joinpath("test1", "test_file_2.1.txt")
|
||||||
.exists()
|
.exists()
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
not (await repository.path(session, config))
|
not (await repository.real_path(session, config))
|
||||||
.joinpath("test1", "test_file_rename.txt")
|
.joinpath("test1", "test_file_rename.txt")
|
||||||
.exists()
|
.exists()
|
||||||
)
|
)
|
||||||
@ -262,12 +266,12 @@ async def test_file(data, tmpdir, session: SessionContext, config: Config):
|
|||||||
await session.refresh(file, attribute_names=["parent"])
|
await session.refresh(file, attribute_names=["parent"])
|
||||||
assert file.parent == directory2
|
assert file.parent == directory2
|
||||||
assert (
|
assert (
|
||||||
not (await repository.path(session, config))
|
not (await repository.real_path(session, config))
|
||||||
.joinpath("test1", "test_file_2.1.txt")
|
.joinpath("test1", "test_file_2.1.txt")
|
||||||
.exists()
|
.exists()
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
(await repository.path(session, config))
|
(await repository.real_path(session, config))
|
||||||
.joinpath("test2", "test_file_2.1.txt")
|
.joinpath("test2", "test_file_2.1.txt")
|
||||||
.exists()
|
.exists()
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user