mirror of https://github.com/apache/cloudstack.git
124 lines
3.9 KiB
Python
124 lines
3.9 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import os
|
|
from io import BufferedReader
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from ..constants import CHUNK_SIZE
|
|
from .base import BackendSession, ImageBackend
|
|
|
|
|
|
class FileSession(BackendSession):
|
|
"""
|
|
Holds a single file handle open for the duration of a streaming read.
|
|
Returns empty bytes at EOF (file semantics).
|
|
"""
|
|
|
|
def __init__(self, path: str):
|
|
self._path = path
|
|
self._fh: Optional[BufferedReader] = open(path, "rb")
|
|
self._size = os.path.getsize(path)
|
|
|
|
def size(self) -> int:
|
|
return self._size
|
|
|
|
def read(self, offset: int, length: int) -> bytes:
|
|
if self._fh is None:
|
|
raise RuntimeError("session is closed")
|
|
self._fh.seek(offset)
|
|
return self._fh.read(length)
|
|
|
|
def close(self) -> None:
|
|
if self._fh is not None:
|
|
self._fh.close()
|
|
self._fh = None
|
|
|
|
|
|
class FileBackend(ImageBackend):
|
|
"""
|
|
ImageBackend implementation backed by a local file (qcow2 or raw).
|
|
Supports full read/write and flush. Does not support extents or range writes.
|
|
"""
|
|
|
|
def __init__(self, file_path: str):
|
|
self._path = file_path
|
|
|
|
@property
|
|
def supports_extents(self) -> bool:
|
|
return False
|
|
|
|
@property
|
|
def supports_range_write(self) -> bool:
|
|
return False
|
|
|
|
def size(self) -> int:
|
|
return os.path.getsize(self._path)
|
|
|
|
def read(self, offset: int, length: int) -> bytes:
|
|
with open(self._path, "rb") as f:
|
|
f.seek(offset)
|
|
return f.read(length)
|
|
|
|
def write(self, data: bytes, offset: int) -> None:
|
|
raise NotImplementedError("file backend does not support range writes")
|
|
|
|
def write_full(self, stream: Any, content_length: int, flush: bool) -> int:
|
|
bytes_written = 0
|
|
remaining = content_length
|
|
with open(self._path, "wb") as f:
|
|
while remaining > 0:
|
|
chunk = stream.read(min(CHUNK_SIZE, remaining))
|
|
if not chunk:
|
|
raise IOError(
|
|
f"request body ended early at {bytes_written} bytes"
|
|
)
|
|
f.write(chunk)
|
|
bytes_written += len(chunk)
|
|
remaining -= len(chunk)
|
|
if flush:
|
|
f.flush()
|
|
os.fsync(f.fileno())
|
|
return bytes_written
|
|
|
|
def flush(self) -> None:
|
|
with open(self._path, "rb") as f:
|
|
f.flush()
|
|
os.fsync(f.fileno())
|
|
|
|
def zero(self, offset: int, length: int) -> None:
|
|
raise NotImplementedError("file backend does not support zero")
|
|
|
|
def get_capabilities(self) -> Dict[str, bool]:
|
|
return {
|
|
"read_only": False,
|
|
"can_flush": True,
|
|
"can_zero": False,
|
|
}
|
|
|
|
def get_allocation_extents(self) -> List[Dict[str, Any]]:
|
|
raise NotImplementedError("file backend does not support extents")
|
|
|
|
def get_dirty_extents(self, dirty_bitmap_context: str) -> List[Dict[str, Any]]:
|
|
raise NotImplementedError("file backend does not support extents")
|
|
|
|
def open_session(self) -> FileSession:
|
|
return FileSession(self._path)
|
|
|
|
def close(self) -> None:
|
|
pass
|