mirror of
https://github.com/koodo-reader/koodo-reader.git
synced 2026-06-18 12:50:44 -04:00
feat: enhance Docker support, improve file upload handling, and update UI for Docker configuration
This commit is contained in:
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
2
src/assets/lib/kookit-extra-browser.min.js
vendored
2
src/assets/lib/kookit-extra-browser.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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": "用户名",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
Reference in New Issue
Block a user