feat: enhance Docker support, improve file upload handling, and update UI for Docker configuration

This commit is contained in:
troyeguo
2025-06-04 16:39:30 +08:00
parent 8ba74a80a8
commit 95e355ffb7
7 changed files with 103 additions and 23 deletions

View File

@@ -59,8 +59,8 @@ CMD ["/start.sh"]
# --name koodo-reader \
# -p 80:80 \
# -p 8000:8000 \
# -e SERVER_ENABLED=false \
# -e ENABLE_HTTP_SERVER=false \
# -e SERVER_USERNAME=admin \
# -e SERVER_PASSWORD=securePass123 \
# -v /path/to/host/uploads:/app/uploads \
# koodo-reader:master
# koodo-reader-test:latest

View File

@@ -108,13 +108,21 @@ function resolveSafePath(...pathSegments) {
// 文件上传处理
function handleUpload(req, res, dirParam) {
const boundary = req.headers['content-type']?.split('=')[1];
if (!boundary) {
const contentType = req.headers['content-type'];
if (!contentType || !contentType.includes('multipart/form-data')) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
return res.end('Invalid Content-Type');
return res.end('Invalid Content-Type. Expected multipart/form-data');
}
const boundaryMatch = contentType.match(/boundary=(.+)$/);
if (!boundaryMatch) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
return res.end('Missing boundary in Content-Type');
}
const boundary = boundaryMatch[1];
let body = [];
req.on('data', (chunk) => body.push(chunk));
req.on('end', () => {
try {
@@ -122,6 +130,7 @@ function handleUpload(req, res, dirParam) {
const parts = parseMultipart(buffer, boundary);
if (!parts.file || !parts.filename) {
console.log('Parsed parts:', Object.keys(parts)); // 调试信息
throw new Error('No valid file uploaded');
}
@@ -166,38 +175,53 @@ function handleUpload(req, res, dirParam) {
});
}
// 解析multipart数据
// 解析multipart数据 - 改进版本
function parseMultipart(buffer, boundary) {
const result = {};
const boundaryPrefix = `--${boundary}`;
const sections = buffer.toString('binary').split(boundaryPrefix);
const boundaryBuffer = Buffer.from(`--${boundary}`);
const parts = [];
for (const section of sections) {
if (!section.trim() || section.includes('--')) continue;
let start = 0;
let end = buffer.indexOf(boundaryBuffer, start);
const headerEnd = section.indexOf('\r\n\r\n');
if (headerEnd === -1) continue;
while (end !== -1) {
if (start !== 0) { // 跳过第一个边界前的内容
parts.push(buffer.slice(start, end));
}
start = end + boundaryBuffer.length;
end = buffer.indexOf(boundaryBuffer, start);
}
const headers = section.substring(0, headerEnd);
const content = section.substring(headerEnd + 4).trim();
for (const part of parts) {
if (part.length === 0) continue;
// 查找头部结束位置
const headerEndIndex = part.indexOf('\r\n\r\n');
if (headerEndIndex === -1) continue;
const headers = part.slice(0, headerEndIndex).toString();
const content = part.slice(headerEndIndex + 4);
// 移除结尾的 \r\n
const actualContent = content.slice(0, content.length - 2);
const nameMatch = headers.match(/name="([^"]+)"/);
const filenameMatch = headers.match(/filename="([^"]+)"/);
if (nameMatch) {
const name = nameMatch[1];
if (filenameMatch) {
if (filenameMatch && filenameMatch[1]) {
result.filename = filenameMatch[1];
result.file = Buffer.from(content, 'binary');
result.file = actualContent;
console.log(`Found file: ${result.filename}, size: ${actualContent.length} bytes`); // 调试信息
} else {
result[name] = content;
result[name] = actualContent.toString();
}
}
}
return result;
}
// 文件下载处理
function handleDownload(req, res, dirParam) {
try {

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -251,6 +251,7 @@
"Margin": "页边距",
"Do you want to open this link in browser": "是否使用浏览器打开此链接",
"WebDAV Info": "WebDAV是一种简单高效的数据备份方式这里推荐使用坚果云提供的WebDAV服务",
"The Koodo Reader Docker version does not support the data source feature by default. You need to modify the configuration parameters during deployment to manually enable it": "Koodo Reader Docker 版默认不支持数据源功能,您需要在部署时修改配置参数手动开启",
"Please wait": "请稍侯",
"Server address": "服务器地址",
"Username": "用户名",

View File

@@ -71,6 +71,14 @@ export const driveList = [
support: ["desktop", "browser", "phone"],
scoped: false,
},
{
label: "Docker",
value: "docker",
icon: "icon-docker",
isPro: true,
support: ["desktop", "browser", "phone"],
scoped: true,
},
{
label: "FTP",
value: "ftp",
@@ -129,6 +137,27 @@ export const driveInputConfig: DriveInputConfig = {
required: true,
},
],
docker: [
{
label: "Server address",
value: "url",
type: "text",
required: true,
example: "http://192.168.28.14:8000",
},
{
label: "Username",
value: "username",
type: "text",
required: true,
},
{
label: "Password",
value: "password",
type: "password",
required: true,
},
],
mega: [
{
label: "Email",

View File

@@ -176,6 +176,7 @@ class SyncSetting extends React.Component<SettingInfoProps, SettingInfoState> {
}
if (
this.props.settingDrive === "webdav" ||
this.props.settingDrive === "docker" ||
this.props.settingDrive === "ftp" ||
this.props.settingDrive === "sftp" ||
this.props.settingDrive === "mega" ||
@@ -272,6 +273,7 @@ class SyncSetting extends React.Component<SettingInfoProps, SettingInfoState> {
}}
>
{this.props.settingDrive === "webdav" ||
this.props.settingDrive === "docker" ||
this.props.settingDrive === "ftp" ||
this.props.settingDrive === "sftp" ||
this.props.settingDrive === "mega" ||
@@ -279,7 +281,7 @@ class SyncSetting extends React.Component<SettingInfoProps, SettingInfoState> {
<>
{driveInputConfig[this.props.settingDrive].map((item) => {
return (
<>
<div key={item.value}>
<input
type={item.type}
name={item.value}
@@ -310,11 +312,18 @@ class SyncSetting extends React.Component<SettingInfoProps, SettingInfoState> {
className="token-dialog-username-box"
/>
{item.example && (
<div style={{ marginTop: "5px", fontSize: "12px" }}>
<div
style={{
marginTop: "5px",
marginLeft: "2px",
fontSize: "12px",
opacity: 0.8,
}}
>
{this.props.t("Example")}: {item.example}
</div>
)}
</>
</div>
);
})}
</>
@@ -357,6 +366,21 @@ class SyncSetting extends React.Component<SettingInfoProps, SettingInfoState> {
)}
</div>
)}
{this.props.settingDrive === "docker" && !isElectron && (
<div
className="token-dialog-tip"
style={{
marginTop: "10px",
fontSize: "13px",
lineHeight: "16px",
color: "rgba(231, 69, 69, 0.8)",
}}
>
{this.props.t(
"The Koodo Reader Docker version does not support the data source feature by default. You need to modify the configuration parameters during deployment to manually enable it"
)}
</div>
)}
{this.props.settingDrive === "s3compatible" && !isElectron && (
<div
className="token-dialog-tip"
@@ -377,6 +401,7 @@ class SyncSetting extends React.Component<SettingInfoProps, SettingInfoState> {
className="voice-add-confirm"
onClick={async () => {
if (
this.props.settingDrive === "docker" ||
this.props.settingDrive === "webdav" ||
this.props.settingDrive === "s3compatible"
) {
@@ -422,6 +447,7 @@ class SyncSetting extends React.Component<SettingInfoProps, SettingInfoState> {
</div>
)}
{(this.props.settingDrive === "webdav" ||
this.props.settingDrive === "docker" ||
this.props.settingDrive === "ftp" ||
this.props.settingDrive === "sftp" ||
this.props.settingDrive === "mega" ||