mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-12 01:00:29 +08:00
reading images api
This commit is contained in:
parent
de14053725
commit
0e3ebac362
@ -59,6 +59,15 @@ location /api/ws {
|
|||||||
auth_request off;
|
auth_request off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /api/msd/read {
|
||||||
|
rewrite ^/api/msd/read$ /msd/read break;
|
||||||
|
rewrite ^/api/msd/read\?(.*)$ /msd/read?$1 break;
|
||||||
|
proxy_pass http://kvmd;
|
||||||
|
include /etc/kvmd/nginx/loc-proxy.conf;
|
||||||
|
proxy_read_timeout 7d;
|
||||||
|
auth_request off;
|
||||||
|
}
|
||||||
|
|
||||||
location /api/msd/write_remote {
|
location /api/msd/write_remote {
|
||||||
rewrite ^/api/msd/write_remote$ /msd/write_remote break;
|
rewrite ^/api/msd/write_remote$ /msd/write_remote break;
|
||||||
rewrite ^/api/msd/write_remote\?(.*)$ /msd/write_remote?$1 break;
|
rewrite ^/api/msd/write_remote\?(.*)$ /msd/write_remote?$1 break;
|
||||||
|
|||||||
@ -84,6 +84,18 @@ class MsdApi:
|
|||||||
|
|
||||||
# =====
|
# =====
|
||||||
|
|
||||||
|
@exposed_http("GET", "/msd/read")
|
||||||
|
async def __read_handler(self, request: Request) -> StreamResponse:
|
||||||
|
name = valid_msd_image_name(request.query.get("image"))
|
||||||
|
async with self.__msd.read_image(name) as size:
|
||||||
|
response = await start_streaming(request, "application/octet-stream", size, name)
|
||||||
|
while True:
|
||||||
|
chunk = await self.__msd.read_image_chunk()
|
||||||
|
if not chunk:
|
||||||
|
return response
|
||||||
|
await response.write(chunk)
|
||||||
|
return response
|
||||||
|
|
||||||
@exposed_http("POST", "/msd/write")
|
@exposed_http("POST", "/msd/write")
|
||||||
async def __write_handler(self, request: Request) -> Response:
|
async def __write_handler(self, request: Request) -> Response:
|
||||||
name = valid_msd_image_name(request.query.get("image"))
|
name = valid_msd_image_name(request.query.get("image"))
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import asyncio
|
|||||||
import contextlib
|
import contextlib
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import inspect
|
import inspect
|
||||||
|
import urllib.parse
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
@ -187,8 +188,20 @@ def make_json_exception(err: Exception, status: Optional[int]=None) -> Response:
|
|||||||
}, status=status)
|
}, status=status)
|
||||||
|
|
||||||
|
|
||||||
async def start_streaming(request: Request, content_type: str) -> StreamResponse:
|
async def start_streaming(
|
||||||
response = StreamResponse(status=200, reason="OK", headers={"Content-Type": content_type})
|
request: Request,
|
||||||
|
content_type: str,
|
||||||
|
content_length: int=-1,
|
||||||
|
file_name: str="",
|
||||||
|
) -> StreamResponse:
|
||||||
|
|
||||||
|
response = StreamResponse(status=200, reason="OK")
|
||||||
|
response.content_type = content_type
|
||||||
|
if content_length >= 0:
|
||||||
|
response.content_length = content_length
|
||||||
|
if file_name:
|
||||||
|
file_name = urllib.parse.quote(file_name, safe="")
|
||||||
|
response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{file_name}"
|
||||||
await response.prepare(request)
|
await response.prepare(request)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@ -131,6 +131,15 @@ class BaseMsd(BasePlugin):
|
|||||||
async def set_connected(self, connected: bool) -> None:
|
async def set_connected(self, connected: bool) -> None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def read_image(self, name: str) -> AsyncGenerator[int, None]: # pylint: disable=unused-argument
|
||||||
|
if self is not None: # XXX: Vulture and pylint hack
|
||||||
|
raise NotImplementedError()
|
||||||
|
yield 1
|
||||||
|
|
||||||
|
async def read_image_chunk(self) -> bytes:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def write_image(self, name: str, size: int) -> AsyncGenerator[int, None]: # pylint: disable=unused-argument
|
async def write_image(self, name: str, size: int) -> AsyncGenerator[int, None]: # pylint: disable=unused-argument
|
||||||
if self is not None: # XXX: Vulture and pylint hack
|
if self is not None: # XXX: Vulture and pylint hack
|
||||||
@ -144,6 +153,40 @@ class BaseMsd(BasePlugin):
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class MsdImageReader:
|
||||||
|
def __init__(self, path: str, chunk_size: int) -> None:
|
||||||
|
self.__name = os.path.basename(path)
|
||||||
|
self.__path = path
|
||||||
|
self.__chunk_size = chunk_size
|
||||||
|
|
||||||
|
self.__file: Optional[aiofiles.base.AiofilesContextManager] = None
|
||||||
|
self.__file_size: int = 0
|
||||||
|
|
||||||
|
async def open(self) -> "MsdImageReader":
|
||||||
|
assert self.__file is None
|
||||||
|
get_logger(1).info("Reading %r image from MSD ...", self.__name)
|
||||||
|
self.__file_size = os.stat(self.__path).st_size
|
||||||
|
self.__file = await aiofiles.open(self.__path, mode="rb") # type: ignore
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_size(self) -> int:
|
||||||
|
assert self.__file is not None
|
||||||
|
return self.__file_size
|
||||||
|
|
||||||
|
async def read(self) -> bytes:
|
||||||
|
assert self.__file is not None
|
||||||
|
return (await self.__file.read(self.__chunk_size)) # type: ignore
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
assert self.__file is not None
|
||||||
|
logger = get_logger()
|
||||||
|
logger.info("Closed image reader ...")
|
||||||
|
try:
|
||||||
|
await self.__file.close() # type: ignore
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Can't close image reader")
|
||||||
|
|
||||||
|
|
||||||
class MsdImageWriter:
|
class MsdImageWriter:
|
||||||
def __init__(self, path: str, size: int, sync: int) -> None:
|
def __init__(self, path: str, size: int, sync: int) -> None:
|
||||||
self.__name = os.path.basename(path)
|
self.__name = os.path.basename(path)
|
||||||
|
|||||||
@ -76,6 +76,15 @@ class Plugin(BaseMsd):
|
|||||||
async def set_connected(self, connected: bool) -> None:
|
async def set_connected(self, connected: bool) -> None:
|
||||||
raise MsdDisabledError()
|
raise MsdDisabledError()
|
||||||
|
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def read_image(self, name: str) -> AsyncGenerator[int, None]:
|
||||||
|
if self is not None: # XXX: Vulture and pylint hack
|
||||||
|
raise MsdDisabledError()
|
||||||
|
yield 1
|
||||||
|
|
||||||
|
async def read_image_chunk(self) -> bytes:
|
||||||
|
raise MsdDisabledError()
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def write_image(self, name: str, size: int) -> AsyncGenerator[int, None]:
|
async def write_image(self, name: str, size: int) -> AsyncGenerator[int, None]:
|
||||||
if self is not None: # XXX: Vulture and pylint hack
|
if self is not None: # XXX: Vulture and pylint hack
|
||||||
|
|||||||
@ -57,6 +57,7 @@ from .. import MsdImageNotSelected
|
|||||||
from .. import MsdUnknownImageError
|
from .. import MsdUnknownImageError
|
||||||
from .. import MsdImageExistsError
|
from .. import MsdImageExistsError
|
||||||
from .. import BaseMsd
|
from .. import BaseMsd
|
||||||
|
from .. import MsdImageReader
|
||||||
from .. import MsdImageWriter
|
from .. import MsdImageWriter
|
||||||
|
|
||||||
from . import fs
|
from . import fs
|
||||||
@ -136,6 +137,7 @@ class _State:
|
|||||||
class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
||||||
def __init__( # pylint: disable=super-init-not-called
|
def __init__( # pylint: disable=super-init-not-called
|
||||||
self,
|
self,
|
||||||
|
read_chunk_size: int,
|
||||||
write_chunk_size: int,
|
write_chunk_size: int,
|
||||||
sync_chunk_size: int,
|
sync_chunk_size: int,
|
||||||
|
|
||||||
@ -148,6 +150,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
gadget: str, # XXX: Not from options, see /kvmd/apps/kvmd/__init__.py for details
|
gadget: str, # XXX: Not from options, see /kvmd/apps/kvmd/__init__.py for details
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
self.__read_chunk_size = read_chunk_size
|
||||||
self.__write_chunk_size = write_chunk_size
|
self.__write_chunk_size = write_chunk_size
|
||||||
self.__sync_chunk_size = sync_chunk_size
|
self.__sync_chunk_size = sync_chunk_size
|
||||||
|
|
||||||
@ -162,6 +165,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
self.__drive = Drive(gadget, instance=0, lun=0)
|
self.__drive = Drive(gadget, instance=0, lun=0)
|
||||||
|
|
||||||
|
self.__reader: Optional[MsdImageReader] = None
|
||||||
self.__writer: Optional[MsdImageWriter] = None
|
self.__writer: Optional[MsdImageWriter] = None
|
||||||
self.__writer_tick = 0.0
|
self.__writer_tick = 0.0
|
||||||
|
|
||||||
@ -175,6 +179,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_plugin_options(cls) -> Dict:
|
def get_plugin_options(cls) -> Dict:
|
||||||
return {
|
return {
|
||||||
|
"read_chunk_size": Option(65536, type=functools.partial(valid_number, min=1024)),
|
||||||
"write_chunk_size": Option(65536, type=functools.partial(valid_number, min=1024)),
|
"write_chunk_size": Option(65536, type=functools.partial(valid_number, min=1024)),
|
||||||
"sync_chunk_size": Option(4194304, type=functools.partial(valid_number, min=1024)),
|
"sync_chunk_size": Option(4194304, type=functools.partial(valid_number, min=1024)),
|
||||||
|
|
||||||
@ -253,6 +258,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
@aiotools.atomic
|
@aiotools.atomic
|
||||||
async def cleanup(self) -> None:
|
async def cleanup(self) -> None:
|
||||||
|
await self.__close_reader()
|
||||||
await self.__close_writer()
|
await self.__close_writer()
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
@ -317,6 +323,29 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
self.__state.vd.connected = connected
|
self.__state.vd.connected = connected
|
||||||
|
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def read_image(self, name: str) -> AsyncGenerator[int, None]:
|
||||||
|
async with self.__state.busy():
|
||||||
|
assert self.__state.storage
|
||||||
|
assert self.__state.vd
|
||||||
|
|
||||||
|
if self.__state.vd.connected or self.__drive.get_image_path():
|
||||||
|
raise MsdConnectedError()
|
||||||
|
|
||||||
|
path = os.path.join(self.__images_path, name)
|
||||||
|
if name not in self.__state.storage.images or not os.path.exists(path):
|
||||||
|
raise MsdUnknownImageError()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.__reader = await MsdImageReader(path, self.__read_chunk_size).open()
|
||||||
|
yield self.__reader.get_size()
|
||||||
|
finally:
|
||||||
|
await self.__close_reader()
|
||||||
|
|
||||||
|
async def read_image_chunk(self) -> bytes:
|
||||||
|
assert self.__reader
|
||||||
|
return (await self.__reader.read())
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def write_image(self, name: str, size: int) -> AsyncGenerator[int, None]:
|
async def write_image(self, name: str, size: int) -> AsyncGenerator[int, None]:
|
||||||
try:
|
try:
|
||||||
@ -387,6 +416,11 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
# =====
|
# =====
|
||||||
|
|
||||||
|
async def __close_reader(self) -> None:
|
||||||
|
if self.__reader:
|
||||||
|
await self.__reader.close()
|
||||||
|
self.__reader = None
|
||||||
|
|
||||||
async def __close_writer(self) -> None:
|
async def __close_writer(self) -> None:
|
||||||
if self.__writer:
|
if self.__writer:
|
||||||
await self.__writer.close()
|
await self.__writer.close()
|
||||||
|
|||||||
@ -217,6 +217,17 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
get_logger(0).info("MSD switched to KVM: %s", self.__device_info)
|
get_logger(0).info("MSD switched to KVM: %s", self.__device_info)
|
||||||
self.__connected = connected
|
self.__connected = connected
|
||||||
|
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def read_image(self, name: str) -> AsyncGenerator[int, None]:
|
||||||
|
async with self.__working():
|
||||||
|
if self is not None: # XXX: Vulture and pylint hack
|
||||||
|
raise MsdMultiNotSupported()
|
||||||
|
yield 1
|
||||||
|
|
||||||
|
async def read_image_chunk(self) -> bytes:
|
||||||
|
async with self.__working():
|
||||||
|
raise MsdMultiNotSupported()
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def write_image(self, name: str, size: int) -> AsyncGenerator[int, None]:
|
async def write_image(self, name: str, size: int) -> AsyncGenerator[int, None]:
|
||||||
async with self.__working():
|
async with self.__working():
|
||||||
|
|||||||
@ -419,6 +419,9 @@
|
|||||||
<td width="100%">
|
<td width="100%">
|
||||||
<select disabled id="msd-image-selector"></select>
|
<select disabled id="msd-image-selector"></select>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<button disabled id="msd-download-button" title="Download image"> 🖪 </button>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button disabled id="msd-remove-button" title="Remove image"><b> × </b></button>
|
<button disabled id="msd-remove-button" title="Remove image"><b> × </b></button>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -37,6 +37,7 @@ li(id="msd-dropdown" class="right feature-disabled")
|
|||||||
tr
|
tr
|
||||||
td Image:
|
td Image:
|
||||||
td(width="100%") #[select(disabled id="msd-image-selector")]
|
td(width="100%") #[select(disabled id="msd-image-selector")]
|
||||||
|
td #[button(disabled id="msd-download-button" title="Download image") 🖪 ]
|
||||||
td #[button(disabled id="msd-remove-button" title="Remove image") #[b × ]]
|
td #[button(disabled id="msd-remove-button" title="Remove image") #[b × ]]
|
||||||
table(class="kv msd-cdrom-emulation feature-disabled")
|
table(class="kv msd-cdrom-emulation feature-disabled")
|
||||||
tr
|
tr
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export function Msd() {
|
|||||||
$("msd-led").title = "Unknown state";
|
$("msd-led").title = "Unknown state";
|
||||||
|
|
||||||
$("msd-image-selector").onchange = __selectImage;
|
$("msd-image-selector").onchange = __selectImage;
|
||||||
|
tools.el.setOnClick($("msd-download-button"), __clickDownloadButton);
|
||||||
tools.el.setOnClick($("msd-remove-button"), __clickRemoveButton);
|
tools.el.setOnClick($("msd-remove-button"), __clickRemoveButton);
|
||||||
|
|
||||||
tools.radio.setOnClick("msd-mode-radio", __clickModeRadio);
|
tools.radio.setOnClick("msd-mode-radio", __clickModeRadio);
|
||||||
@ -67,10 +68,16 @@ export function Msd() {
|
|||||||
|
|
||||||
var __selectImage = function() {
|
var __selectImage = function() {
|
||||||
tools.el.setEnabled($("msd-image-selector"), false);
|
tools.el.setEnabled($("msd-image-selector"), false);
|
||||||
|
tools.el.setEnabled($("msd-download-button"), false);
|
||||||
tools.el.setEnabled($("msd-remove-button"), false);
|
tools.el.setEnabled($("msd-remove-button"), false);
|
||||||
__sendParam("image", $("msd-image-selector").value);
|
__sendParam("image", $("msd-image-selector").value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var __clickDownloadButton = function() {
|
||||||
|
let name = $("msd-image-selector").value;
|
||||||
|
window.open(`/api/msd/read?image=${name}`);
|
||||||
|
};
|
||||||
|
|
||||||
var __clickRemoveButton = function() {
|
var __clickRemoveButton = function() {
|
||||||
let name = $("msd-image-selector").value;
|
let name = $("msd-image-selector").value;
|
||||||
wm.confirm(`Are you sure you want to remove the image<br><b>${name}</b> from PiKVM?`).then(function(ok) {
|
wm.confirm(`Are you sure you want to remove the image<br><b>${name}</b> from PiKVM?`).then(function(ok) {
|
||||||
@ -244,6 +251,7 @@ export function Msd() {
|
|||||||
|
|
||||||
tools.el.setEnabled($("msd-image-selector"), (online && s.features.multi && !s.drive.connected && !s.busy));
|
tools.el.setEnabled($("msd-image-selector"), (online && s.features.multi && !s.drive.connected && !s.busy));
|
||||||
__applyStateImageSelector();
|
__applyStateImageSelector();
|
||||||
|
tools.el.setEnabled($("msd-download-button"), (online && s.features.multi && s.drive.image && !s.drive.connected && !s.busy));
|
||||||
tools.el.setEnabled($("msd-remove-button"), (online && s.features.multi && s.drive.image && !s.drive.connected && !s.busy));
|
tools.el.setEnabled($("msd-remove-button"), (online && s.features.multi && s.drive.image && !s.drive.connected && !s.busy));
|
||||||
|
|
||||||
tools.radio.setEnabled("msd-mode-radio", (online && s.features.cdrom && !s.drive.connected && !s.busy));
|
tools.radio.setEnabled("msd-mode-radio", (online && s.features.cdrom && !s.drive.connected && !s.busy));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user