Allow images to be stored in subdirs and used by templates.

This commit is contained in:
grossmj committed 2021-08-30 16:53:41 +09:30
1 parent 332fa47b50
commit d606553e20
8 files changed
+159 -48

No files matched your search

+31 -21
View File
@@ -23,6 +23,7 @@ import logging
import urllib.parse
from fastapi import APIRouter, Request, Response, Depends, status
from sqlalchemy.orm.exc import MultipleResultsFound
from typing import List
from gns3server import schemas
@@ -53,9 +54,9 @@ async def get_images(
return await images_repo.get_images()
@router.post("/upload/{image_name}", response_model=schemas.Image, status_code=status.HTTP_201_CREATED)
@router.post("/upload/{image_path:path}", response_model=schemas.Image, status_code=status.HTTP_201_CREATED)
async def upload_image(
image_name: str,
image_path: str,
request: Request,
image_type: schemas.ImageType = schemas.ImageType.qemu,
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
@@ -64,19 +65,20 @@ async def upload_image(
Upload an image.
"""
image_name = urllib.parse.unquote(image_name)
image_path = urllib.parse.unquote(image_path)
image_dir, image_name = os.path.split(image_path)
directory = default_images_directory(image_type)
path = os.path.abspath(os.path.join(directory, image_name))
if os.path.commonprefix([directory, path]) != directory:
raise ControllerForbiddenError(f"Could not write image: {image_name}, '{path}' is forbidden")
full_path = os.path.abspath(os.path.join(directory, image_dir, image_name))
if os.path.commonprefix([directory, full_path]) != directory:
raise ControllerForbiddenError(f"Could not write image, '{image_path}' is forbidden")
if await images_repo.get_image(image_name):
raise ControllerBadRequestError(f"Image '{image_name}' already exists")
if await images_repo.get_image(image_path):
raise ControllerBadRequestError(f"Image '{image_path}' already exists")
try:
image = await write_image(image_name, image_type, path, request.stream(), images_repo)
image = await write_image(image_name, image_type, full_path, request.stream(), images_repo)
except (OSError, InvalidImageError) as e:
raise ControllerError(f"Could not save {image_type} image '{image_name}': {e}")
raise ControllerError(f"Could not save {image_type} image '{image_path}': {e}")
# TODO: automatically create template based on image checksum
#from gns3server.controller import Controller
@@ -86,45 +88,53 @@ async def upload_image(
return image
@router.get("/{image_name}", response_model=schemas.Image)
@router.get("/{image_path:path}", response_model=schemas.Image)
async def get_image(
image_name: str,
image_path: str,
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> schemas.Image:
"""
Return an image.
"""
image = await images_repo.get_image(image_name)
image_path = urllib.parse.unquote(image_path)
image = await images_repo.get_image(image_path)
if not image:
raise ControllerNotFoundError(f"Image '{image_name}' not found")
raise ControllerNotFoundError(f"Image '{image_path}' not found")
return image
@router.delete("/{image_name}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete("/{image_path:path}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_image(
image_name: str,
image_path: str,
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> None:
"""
Delete an image.
"""
image = await images_repo.get_image(image_name)
image_path = urllib.parse.unquote(image_path)
try:
image = await images_repo.get_image(image_path)
except MultipleResultsFound:
raise ControllerBadRequestError(f"Image '{image_path}' matches multiple images. "
f"Please include the relative path of the image")
if not image:
raise ControllerNotFoundError(f"Image '{image_name}' not found")
raise ControllerNotFoundError(f"Image '{image_path}' not found")
if await images_repo.get_image_templates(image.id):
raise ControllerError(f"Image '{image_name}' is used by one or more templates")
raise ControllerError(f"Image '{image_path}' is used by one or more templates")
try:
os.remove(image.path)
except OSError:
log.warning(f"Could not delete image file {image.path}")
success = await images_repo.delete_image(image_name)
success = await images_repo.delete_image(image_path)
if not success:
raise ControllerError(f"Image '{image_name}' could not be deleted")
raise ControllerError(f"Image '{image_path}' could not be deleted")
@router.post("/prune", status_code=status.HTTP_204_NO_CONTENT)
+2 -2
View File
@@ -34,10 +34,10 @@ class Image(BaseTable):
__tablename__ = "images"
id = Column(Integer, primary_key=True, autoincrement=True)
filename = Column(String, unique=True, index=True)
filename = Column(String)
image_type = Column(String)
image_size = Column(BigInteger)
path = Column(String)
path = Column(String, unique=True, index=True)
checksum = Column(String)
checksum_algorithm = Column(String)
templates = relationship("Template", secondary=image_template_link, back_populates="images")
+17 -6
View File
@@ -36,14 +36,19 @@ class ImagesRepository(BaseRepository):
super().__init__(db_session)
async def get_image(self, image_name: str) -> Optional[models.Image]:
async def get_image(self, image_path: str) -> Optional[models.Image]:
"""
Get an image by its name (filename).
Get an image by its path.
"""
query = select(models.Image).where(models.Image.filename == image_name)
image_dir, image_name = os.path.split(image_path)
if image_dir:
query = select(models.Image).\
where(models.Image.filename == image_name, models.Image.path.endswith(image_path))
else:
query = select(models.Image).where(models.Image.filename == image_name)
result = await self._db_session.execute(query)
return result.scalars().first()
return result.scalars().one_or_none()
async def get_image_by_checksum(self, checksum: str) -> Optional[models.Image]:
"""
@@ -95,12 +100,18 @@ class ImagesRepository(BaseRepository):
await self._db_session.refresh(db_image)
return db_image
async def delete_image(self, image_name: str) -> bool:
async def delete_image(self, image_path: str) -> bool:
"""
Delete an image.
"""
query = delete(models.Image).where(models.Image.filename == image_name)
image_dir, image_name = os.path.split(image_path)
if image_dir:
query = delete(models.Image).\
where(models.Image.filename == image_name, models.Image.path.endswith(image_path)).\
execution_options(synchronize_session=False)
else:
query = delete(models.Image).where(models.Image.filename == image_name)
result = await self._db_session.execute(query)
await self._db_session.commit()
return result.rowcount > 0
+11 -4
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from uuid import UUID
from typing import List, Union, Optional
from sqlalchemy import select, delete
@@ -102,14 +104,19 @@ class TemplatesRepository(BaseRepository):
await self._db_session.refresh(db_template)
return db_template
async def get_image(self, image_name: str) -> Optional[models.Image]:
async def get_image(self, image_path: str) -> Optional[models.Image]:
"""
Get an image by its name (filename).
Get an image by its path.
"""
query = select(models.Image).where(models.Image.filename == image_name)
image_dir, image_name = os.path.split(image_path)
if image_dir:
query = select(models.Image).\
where(models.Image.filename == image_name, models.Image.path.endswith(image_path))
else:
query = select(models.Image).where(models.Image.filename == image_name)
result = await self._db_session.execute(query)
return result.scalars().first()
return result.scalars().one_or_none()
async def add_image_to_template(
self,
+5 -5
View File
@@ -155,11 +155,11 @@ class TemplatesService:
templates.append(jsonable_encoder(builtin_template))
return templates
async def _find_image(self, image_name):
async def _find_image(self, image_path: str):
image = await self._templates_repo.get_image(image_name)
image = await self._templates_repo.get_image(image_path)
if not image or not os.path.exists(image.path):
raise ControllerNotFoundError(f"Image '{image_name}' could not be found")
raise ControllerNotFoundError(f"Image '{image_path}' could not be found")
return image
async def _find_images(self, template_type: str, settings: dict) -> List[models.Image]:
@@ -228,9 +228,9 @@ class TemplatesService:
raise ControllerNotFoundError(f"Template '{template_id}' not found")
return template
async def _remove_image(self, template_id: UUID, image:str) -> None:
async def _remove_image(self, template_id: UUID, image_path:str) -> None:
image = await self._templates_repo.get_image(image)
image = await self._templates_repo.get_image(image_path)
await self._templates_repo.remove_image_from_template(template_id, image)
async def update_template(self, template_id: UUID, template_update: schemas.TemplateUpdate) -> dict:
+3 -2
View File
@@ -273,8 +273,9 @@ async def write_image(
checksum = checksum.hexdigest()
duplicate_image = await images_repo.get_image_by_checksum(checksum)
if duplicate_image:
raise InvalidImageError(f"Image {duplicate_image.filename} with same checksum already exists")
if duplicate_image and os.path.dirname(duplicate_image.path) == os.path.dirname(path):
raise InvalidImageError(f"Image {duplicate_image.filename} with "
f"same checksum already exists in the same directory")
except InvalidImageError:
os.remove(tmp_path)
raise