Add storage
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.cyynote.controller;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.cyynote.model.UserModel;
|
||||
import com.cyynote.payload.request.LoginRequest;
|
||||
@@ -42,9 +43,8 @@ public class AuthController extends BaseController {
|
||||
}
|
||||
|
||||
log.info("signin username:" + loginRequest.getUsername());
|
||||
UserModel model = (UserModel) ((DbTableQuery) ((DbTableQuery) this.db.table("user")
|
||||
UserModel model = (UserModel) ((DbTableQuery) this.db.table("user")
|
||||
.whereEq("username", loginRequest.getUsername()))
|
||||
.andEq("password", loginRequest.getPassword()))
|
||||
.selectItem("*", UserModel.class);
|
||||
|
||||
if (model == null) {
|
||||
@@ -52,6 +52,29 @@ public class AuthController extends BaseController {
|
||||
return Result.failure(401, "用户名或密码错误");
|
||||
}
|
||||
|
||||
boolean passwordMatch;
|
||||
if (model.getPasswordHashed() == null || model.getPasswordHashed() == 0) {
|
||||
// Plain-text comparison; migrate to hash on success
|
||||
passwordMatch = loginRequest.getPassword().equals(model.getPassword());
|
||||
if (passwordMatch) {
|
||||
String hashed = DigestUtil.sha256Hex(loginRequest.getPassword());
|
||||
((DbTableQuery) this.db.table("user")
|
||||
.set("password", hashed)
|
||||
.set("password_hashed", 1)
|
||||
.whereEq("id", model.getId()))
|
||||
.update();
|
||||
model.setPassword(hashed);
|
||||
model.setPasswordHashed(1);
|
||||
}
|
||||
} else {
|
||||
passwordMatch = DigestUtil.sha256Hex(loginRequest.getPassword()).equals(model.getPassword());
|
||||
}
|
||||
|
||||
if (!passwordMatch) {
|
||||
ctx.status(401);
|
||||
return Result.failure(401, "用户名或密码错误");
|
||||
}
|
||||
|
||||
ctx.sessionSet("user_name", model.getUsername());
|
||||
ctx.sessionSet("user_id", model.getId());
|
||||
String token = ctx.sessionState().sessionToken();
|
||||
@@ -84,17 +107,30 @@ public class AuthController extends BaseController {
|
||||
}
|
||||
|
||||
log.info("update password username:" + loginRequest.getUsername());
|
||||
UserModel model = (UserModel) ((DbTableQuery) ((DbTableQuery) this.db.table("user")
|
||||
UserModel model = (UserModel) ((DbTableQuery) this.db.table("user")
|
||||
.whereEq("username", loginRequest.getUsername()))
|
||||
.andEq("password", loginRequest.getOldpassword()))
|
||||
.selectItem("*", UserModel.class);
|
||||
if (model == null) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "用户名或旧密码错误");
|
||||
}
|
||||
|
||||
boolean oldPasswordMatch;
|
||||
if (model.getPasswordHashed() == null || model.getPasswordHashed() == 0) {
|
||||
oldPasswordMatch = loginRequest.getOldpassword().equals(model.getPassword());
|
||||
} else {
|
||||
oldPasswordMatch = DigestUtil.sha256Hex(loginRequest.getOldpassword()).equals(model.getPassword());
|
||||
}
|
||||
|
||||
if (!oldPasswordMatch) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "用户名或旧密码错误");
|
||||
}
|
||||
|
||||
String newHashed = DigestUtil.sha256Hex(loginRequest.getNewpassword());
|
||||
int flag = ((DbTableQuery) this.db.table("user")
|
||||
.set("password", loginRequest.getNewpassword())
|
||||
.set("password", newHashed)
|
||||
.set("password_hashed", 1)
|
||||
.whereEq("id", model.getId()))
|
||||
.update();
|
||||
return Result.succeed(flag);
|
||||
@@ -120,7 +156,8 @@ public class AuthController extends BaseController {
|
||||
UserModel user = new UserModel();
|
||||
user.setUsername(signUpRequest.getUsername());
|
||||
user.setEmail(signUpRequest.getEmail());
|
||||
user.setPassword(signUpRequest.getPassword());
|
||||
user.setPassword(DigestUtil.sha256Hex(signUpRequest.getPassword()));
|
||||
user.setPasswordHashed(1);
|
||||
user.setRole("ROLE_USER");
|
||||
this.db.table("user").setEntityIf(user, (k, v) -> Boolean.valueOf(v != null)).insert();
|
||||
return Result.succeed("注册成功");
|
||||
@@ -145,4 +182,3 @@ public class AuthController extends BaseController {
|
||||
return List.of("ROLE_USER");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
package com.cyynote.controller;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.cyynote.model.DriveFileModel;
|
||||
import com.cyynote.payload.request.NoteRequest;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
import org.noear.solon.annotation.Body;
|
||||
import org.noear.solon.annotation.Controller;
|
||||
import org.noear.solon.annotation.Get;
|
||||
import org.noear.solon.annotation.Mapping;
|
||||
import org.noear.solon.annotation.Post;
|
||||
import org.noear.solon.core.handle.Context;
|
||||
import org.noear.solon.core.handle.Result;
|
||||
import org.noear.solon.core.handle.UploadedFile;
|
||||
import org.noear.wood.DataItem;
|
||||
import org.noear.wood.DbContext;
|
||||
import org.noear.wood.DbTableQuery;
|
||||
import org.noear.wood.annotation.Db;
|
||||
import org.noear.solon.sessionstate.jwt.JwtUtils;
|
||||
|
||||
@Mapping("/api/drive")
|
||||
@Controller
|
||||
public class DriveController extends BaseController {
|
||||
private static final Logger log = Logger.getLogger(DriveController.class.getName());
|
||||
private static final String DEFAULT_LOCAL_DIR = "./local_drive";
|
||||
|
||||
@Db
|
||||
DbContext db;
|
||||
|
||||
/** List files for current user */
|
||||
@Get
|
||||
@Mapping("/list")
|
||||
public Result listFiles(Context ctx) throws SQLException {
|
||||
String userId = getUserInfoId(ctx);
|
||||
DbTableQuery q = this.db.table("drive_file").whereEq("flag", 1);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
List<DriveFileModel> files = q.orderByDesc("createtime")
|
||||
.selectList("id,filename,original_name,size,mime_type,share_token,share_expiry,createtime,userid,storage_mode,local_path", DriveFileModel.class);
|
||||
return Result.succeed(JSONArray.parseArray(JSONArray.toJSONString(files)));
|
||||
}
|
||||
|
||||
/** Upload file to local server storage */
|
||||
@Post
|
||||
@Mapping("/upload-local")
|
||||
public Result uploadLocal(Context ctx) throws SQLException, IOException {
|
||||
String userId = getUserInfoId(ctx);
|
||||
if (userId == null) {
|
||||
ctx.status(401);
|
||||
return Result.failure(401, "未登录");
|
||||
}
|
||||
UploadedFile uploadedFile = ctx.file("file");
|
||||
if (uploadedFile == null) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "未选择文件");
|
||||
}
|
||||
|
||||
// Resolve storage directory from settings, fallback to default
|
||||
String localDir = getLocalStoragePath();
|
||||
Path dirPath = Paths.get(localDir);
|
||||
Files.createDirectories(dirPath);
|
||||
|
||||
// Generate unique filename to avoid collisions
|
||||
String ext = "";
|
||||
String originalName = uploadedFile.getName();
|
||||
int dotIdx = originalName.lastIndexOf('.');
|
||||
if (dotIdx >= 0) ext = originalName.substring(dotIdx);
|
||||
String uniqueFilename = IdUtil.fastSimpleUUID() + ext;
|
||||
Path filePath = dirPath.resolve(uniqueFilename);
|
||||
|
||||
// Save file
|
||||
Files.copy(uploadedFile.getContent(), filePath);
|
||||
|
||||
DriveFileModel file = new DriveFileModel();
|
||||
file.setFilename(uniqueFilename);
|
||||
file.setOriginalName(originalName);
|
||||
file.setSize(uploadedFile.getSize());
|
||||
file.setMimeType(uploadedFile.getContentType());
|
||||
file.setStorageMode("local");
|
||||
file.setLocalPath(filePath.toString());
|
||||
file.setUserid(Integer.valueOf(userId));
|
||||
file.setCreatetime(DateUtil.now());
|
||||
file.setFlag(1);
|
||||
this.db.table("drive_file").setEntityIf(file, (k, v) -> Boolean.valueOf(v != null)).insert();
|
||||
return Result.succeed("ok");
|
||||
}
|
||||
|
||||
/** Download a local file by record id */
|
||||
@Get
|
||||
@Mapping("/download/{id}")
|
||||
public void downloadLocal(Context ctx, int id) throws SQLException, IOException {
|
||||
String userId = getUserInfoId(ctx);
|
||||
DbTableQuery q = this.db.table("drive_file").whereEq("id", id).whereEq("flag", 1);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
DriveFileModel file = q.selectItem("id,original_name,local_path,mime_type,storage_mode", DriveFileModel.class);
|
||||
if (file == null || !"local".equals(file.getStorageMode())) {
|
||||
ctx.status(404);
|
||||
ctx.output("文件不存在");
|
||||
return;
|
||||
}
|
||||
File f = new File(file.getLocalPath());
|
||||
if (!f.exists()) {
|
||||
ctx.status(404);
|
||||
ctx.output("文件已被移除");
|
||||
return;
|
||||
}
|
||||
String mimeType = file.getMimeType() != null ? file.getMimeType() : "application/octet-stream";
|
||||
String encodedName = URLEncoder.encode(file.getOriginalName(), StandardCharsets.UTF_8).replace("+", "%20");
|
||||
ctx.headerSet("Content-Type", mimeType);
|
||||
ctx.headerSet("Content-Disposition", "attachment; filename*=UTF-8''" + encodedName);
|
||||
ctx.headerSet("Content-Length", String.valueOf(f.length()));
|
||||
try (FileInputStream fis = new FileInputStream(f); OutputStream out = ctx.outputStream()) {
|
||||
fis.transferTo(out);
|
||||
}
|
||||
}
|
||||
|
||||
/** Register a file record (called after frontend uploads directly to R2) */
|
||||
@Post
|
||||
@Mapping("/register")
|
||||
public Result registerFile(Context ctx, @Body JSONObject body) throws SQLException {
|
||||
if (body == null) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "参数不能为空");
|
||||
}
|
||||
String userId = getUserInfoId(ctx);
|
||||
if (userId == null) {
|
||||
ctx.status(401);
|
||||
return Result.failure(401, "未登录");
|
||||
}
|
||||
DriveFileModel file = new DriveFileModel();
|
||||
file.setFilename(body.getString("filename"));
|
||||
file.setOriginalName(body.getString("original_name"));
|
||||
file.setSize(body.getLong("size"));
|
||||
file.setMimeType(body.getString("mime_type"));
|
||||
file.setR2Key(body.getString("r2_key"));
|
||||
file.setStorageMode("r2");
|
||||
file.setUserid(Integer.valueOf(userId));
|
||||
file.setCreatetime(DateUtil.now());
|
||||
file.setFlag(1);
|
||||
this.db.table("drive_file").setEntityIf(file, (k, v) -> Boolean.valueOf(v != null)).insert();
|
||||
return Result.succeed("ok");
|
||||
}
|
||||
|
||||
/** Generate or refresh share link for a file */
|
||||
@Post
|
||||
@Mapping("/share")
|
||||
public Result shareFile(Context ctx, @Body JSONObject body) throws SQLException {
|
||||
if (body == null || body.getInteger("id") == null) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "参数不能为空");
|
||||
}
|
||||
String userId = getUserInfoId(ctx);
|
||||
int fileId = body.getInteger("id");
|
||||
DbTableQuery q = this.db.table("drive_file").whereEq("id", fileId);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
DriveFileModel file = q.selectItem("*", DriveFileModel.class);
|
||||
if (file == null) {
|
||||
ctx.status(404);
|
||||
return Result.failure(404, "文件不存在");
|
||||
}
|
||||
String token = IdUtil.fastSimpleUUID();
|
||||
Integer days = body.getInteger("days");
|
||||
String expiry = (days != null && days > 0)
|
||||
? DateUtil.offsetDay(DateUtil.date(), days).toString()
|
||||
: null;
|
||||
this.db.table("drive_file")
|
||||
.set("share_token", token)
|
||||
.set("share_expiry", expiry)
|
||||
.whereEq("id", fileId)
|
||||
.update();
|
||||
JSONObject result = new JSONObject();
|
||||
result.put("share_token", token);
|
||||
result.put("share_expiry", expiry);
|
||||
return Result.succeed(result);
|
||||
}
|
||||
|
||||
/** Revoke share link */
|
||||
@Post
|
||||
@Mapping("/unshare")
|
||||
public Result unshareFile(Context ctx, @Body JSONObject body) throws SQLException {
|
||||
if (body == null || body.getInteger("id") == null) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "参数不能为空");
|
||||
}
|
||||
String userId = getUserInfoId(ctx);
|
||||
int fileId = body.getInteger("id");
|
||||
DbTableQuery q = this.db.table("drive_file")
|
||||
.set("share_token", null)
|
||||
.set("share_expiry", null)
|
||||
.whereEq("id", fileId);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
q.update();
|
||||
return Result.succeed("ok");
|
||||
}
|
||||
|
||||
/** Soft delete a file */
|
||||
@Post
|
||||
@Mapping("/delete")
|
||||
public Result deleteFile(Context ctx, @Body JSONObject body) throws SQLException {
|
||||
if (body == null || body.getInteger("id") == null) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "参数不能为空");
|
||||
}
|
||||
String userId = getUserInfoId(ctx);
|
||||
int fileId = body.getInteger("id");
|
||||
DbTableQuery q = this.db.table("drive_file").set("flag", 0).whereEq("id", fileId);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
int rows = q.update();
|
||||
if (rows == 0) {
|
||||
ctx.status(404);
|
||||
return Result.failure(404, "文件不存在");
|
||||
}
|
||||
return Result.succeed("ok");
|
||||
}
|
||||
|
||||
/** Public download via share token (no auth required) */
|
||||
@Get
|
||||
@Mapping("/public/{token}")
|
||||
public Result publicDownload(Context ctx, String token) throws SQLException {
|
||||
if (token == null || token.isBlank()) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "无效的分享链接");
|
||||
}
|
||||
DriveFileModel file = this.db.table("drive_file")
|
||||
.whereEq("share_token", token)
|
||||
.whereEq("flag", 1)
|
||||
.selectItem("id,filename,original_name,r2_key,share_expiry,mime_type,storage_mode,local_path", DriveFileModel.class);
|
||||
if (file == null) {
|
||||
ctx.status(404);
|
||||
return Result.failure(404, "分享链接不存在或已失效");
|
||||
}
|
||||
if (file.getShareExpiry() != null && !file.getShareExpiry().isBlank()) {
|
||||
try {
|
||||
if (DateUtil.parseDateTime(file.getShareExpiry()).before(DateUtil.date())) {
|
||||
ctx.status(410);
|
||||
return Result.failure(410, "分享链接已过期");
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
JSONObject result = new JSONObject();
|
||||
result.put("original_name", file.getOriginalName());
|
||||
result.put("r2_key", file.getR2Key());
|
||||
result.put("mime_type", file.getMimeType());
|
||||
result.put("storage_mode", file.getStorageMode());
|
||||
result.put("local_download_url", "local".equals(file.getStorageMode())
|
||||
? "/api/drive/public-download/" + token : null);
|
||||
return Result.succeed(result);
|
||||
}
|
||||
|
||||
/** Public direct download for local files via share token */
|
||||
@Get
|
||||
@Mapping("/public-download/{token}")
|
||||
public void publicLocalDownload(Context ctx, String token) throws SQLException, IOException {
|
||||
DriveFileModel file = this.db.table("drive_file")
|
||||
.whereEq("share_token", token)
|
||||
.whereEq("flag", 1)
|
||||
.selectItem("id,original_name,local_path,mime_type,storage_mode,share_expiry", DriveFileModel.class);
|
||||
if (file == null || !"local".equals(file.getStorageMode())) {
|
||||
ctx.status(404); ctx.output("文件不存在"); return;
|
||||
}
|
||||
if (file.getShareExpiry() != null && !file.getShareExpiry().isBlank()) {
|
||||
try {
|
||||
if (DateUtil.parseDateTime(file.getShareExpiry()).before(DateUtil.date())) {
|
||||
ctx.status(410); ctx.output("分享链接已过期"); return;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
File f = new File(file.getLocalPath());
|
||||
if (!f.exists()) { ctx.status(404); ctx.output("文件已被移除"); return; }
|
||||
String mimeType = file.getMimeType() != null ? file.getMimeType() : "application/octet-stream";
|
||||
String encodedName = URLEncoder.encode(file.getOriginalName(), StandardCharsets.UTF_8).replace("+", "%20");
|
||||
ctx.headerSet("Content-Type", mimeType);
|
||||
ctx.headerSet("Content-Disposition", "attachment; filename*=UTF-8''" + encodedName);
|
||||
ctx.headerSet("Content-Length", String.valueOf(f.length()));
|
||||
try (FileInputStream fis = new FileInputStream(f); OutputStream out = ctx.outputStream()) {
|
||||
fis.transferTo(out);
|
||||
}
|
||||
}
|
||||
|
||||
private String getLocalStoragePath() {
|
||||
try {
|
||||
DataItem row = (DataItem) db.table("settings").whereEq("key", "local_storage_path").selectItem("value", DataItem.class);
|
||||
if (row != null) {
|
||||
String v = row.getString("value");
|
||||
if (v != null && !v.isBlank()) return v;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return DEFAULT_LOCAL_DIR;
|
||||
}
|
||||
|
||||
private String getUserInfoId(Context ctx) {
|
||||
if (ctx.header("Authorization") != null) {
|
||||
String token = ctx.header("Authorization").replace("Bearer ", "");
|
||||
Claims claims = JwtUtils.parseJwt(token);
|
||||
if (claims != null) return claims.get("user_id").toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Mapping("/api/drive")
|
||||
@Controller
|
||||
public class DriveController extends BaseController {
|
||||
private static final Logger log = Logger.getLogger(DriveController.class.getName());
|
||||
|
||||
@Db
|
||||
DbContext db;
|
||||
|
||||
/** List files for current user */
|
||||
@Get
|
||||
@Mapping("/list")
|
||||
public Result listFiles(Context ctx) throws SQLException {
|
||||
String userId = getUserInfoId(ctx);
|
||||
DbTableQuery q = this.db.table("drive_file").whereEq("flag", 1);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
List<DriveFileModel> files = q.orderByDesc("createtime")
|
||||
.selectList("id,filename,original_name,size,mime_type,share_token,share_expiry,createtime,userid", DriveFileModel.class);
|
||||
return Result.succeed(JSONArray.parseArray(JSONArray.toJSONString(files)));
|
||||
}
|
||||
|
||||
/** Register a file record (called after frontend uploads directly to R2, or used for server-side upload) */
|
||||
@Post
|
||||
@Mapping("/register")
|
||||
public Result registerFile(Context ctx, @Body JSONObject body) throws SQLException {
|
||||
if (body == null) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "参数不能为空");
|
||||
}
|
||||
String userId = getUserInfoId(ctx);
|
||||
if (userId == null) {
|
||||
ctx.status(401);
|
||||
return Result.failure(401, "未登录");
|
||||
}
|
||||
DriveFileModel file = new DriveFileModel();
|
||||
file.setFilename(body.getString("filename"));
|
||||
file.setOriginalName(body.getString("original_name"));
|
||||
file.setSize(body.getLong("size"));
|
||||
file.setMimeType(body.getString("mime_type"));
|
||||
file.setR2Key(body.getString("r2_key"));
|
||||
file.setUserid(Integer.valueOf(userId));
|
||||
file.setCreatetime(DateUtil.now());
|
||||
file.setFlag(1);
|
||||
this.db.table("drive_file").setEntityIf(file, (k, v) -> Boolean.valueOf(v != null)).insert();
|
||||
return Result.succeed("ok");
|
||||
}
|
||||
|
||||
/** Generate or refresh share link for a file */
|
||||
@Post
|
||||
@Mapping("/share")
|
||||
public Result shareFile(Context ctx, @Body JSONObject body) throws SQLException {
|
||||
if (body == null || body.getInteger("id") == null) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "参数不能为空");
|
||||
}
|
||||
String userId = getUserInfoId(ctx);
|
||||
int fileId = body.getInteger("id");
|
||||
// Verify ownership
|
||||
DbTableQuery q = this.db.table("drive_file").whereEq("id", fileId);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
DriveFileModel file = q.selectItem("*", DriveFileModel.class);
|
||||
if (file == null) {
|
||||
ctx.status(404);
|
||||
return Result.failure(404, "文件不存在");
|
||||
}
|
||||
String token = IdUtil.fastSimpleUUID();
|
||||
// Expiry: 7 days by default, or "forever" if expiry=0
|
||||
Integer days = body.getInteger("days");
|
||||
String expiry = (days != null && days > 0)
|
||||
? DateUtil.offsetDay(DateUtil.date(), days).toString()
|
||||
: null;
|
||||
this.db.table("drive_file")
|
||||
.set("share_token", token)
|
||||
.set("share_expiry", expiry)
|
||||
.whereEq("id", fileId)
|
||||
.update();
|
||||
JSONObject result = new JSONObject();
|
||||
result.put("share_token", token);
|
||||
result.put("share_expiry", expiry);
|
||||
return Result.succeed(result);
|
||||
}
|
||||
|
||||
/** Revoke share link */
|
||||
@Post
|
||||
@Mapping("/unshare")
|
||||
public Result unshareFile(Context ctx, @Body JSONObject body) throws SQLException {
|
||||
if (body == null || body.getInteger("id") == null) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "参数不能为空");
|
||||
}
|
||||
String userId = getUserInfoId(ctx);
|
||||
int fileId = body.getInteger("id");
|
||||
DbTableQuery q = this.db.table("drive_file")
|
||||
.set("share_token", null)
|
||||
.set("share_expiry", null)
|
||||
.whereEq("id", fileId);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
q.update();
|
||||
return Result.succeed("ok");
|
||||
}
|
||||
|
||||
/** Soft delete a file */
|
||||
@Post
|
||||
@Mapping("/delete")
|
||||
public Result deleteFile(Context ctx, @Body JSONObject body) throws SQLException {
|
||||
if (body == null || body.getInteger("id") == null) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "参数不能为空");
|
||||
}
|
||||
String userId = getUserInfoId(ctx);
|
||||
int fileId = body.getInteger("id");
|
||||
DbTableQuery q = this.db.table("drive_file").set("flag", 0).whereEq("id", fileId);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
int rows = q.update();
|
||||
if (rows == 0) {
|
||||
ctx.status(404);
|
||||
return Result.failure(404, "文件不存在");
|
||||
}
|
||||
return Result.succeed("ok");
|
||||
}
|
||||
|
||||
/** Public download via share token (no auth required) */
|
||||
@Get
|
||||
@Mapping("/public/{token}")
|
||||
public Result publicDownload(Context ctx, String token) throws SQLException {
|
||||
if (token == null || token.isBlank()) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "无效的分享链接");
|
||||
}
|
||||
DriveFileModel file = this.db.table("drive_file")
|
||||
.whereEq("share_token", token)
|
||||
.whereEq("flag", 1)
|
||||
.selectItem("id,filename,original_name,r2_key,share_expiry,mime_type", DriveFileModel.class);
|
||||
if (file == null) {
|
||||
ctx.status(404);
|
||||
return Result.failure(404, "分享链接不存在或已失效");
|
||||
}
|
||||
// Check expiry
|
||||
if (file.getShareExpiry() != null && !file.getShareExpiry().isBlank()) {
|
||||
try {
|
||||
if (DateUtil.parseDateTime(file.getShareExpiry()).before(DateUtil.date())) {
|
||||
ctx.status(410);
|
||||
return Result.failure(410, "分享链接已过期");
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
// Return R2 key for frontend to construct download URL, or redirect to public R2 URL
|
||||
JSONObject result = new JSONObject();
|
||||
result.put("original_name", file.getOriginalName());
|
||||
result.put("r2_key", file.getR2Key());
|
||||
result.put("mime_type", file.getMimeType());
|
||||
return Result.succeed(result);
|
||||
}
|
||||
|
||||
private String getUserInfoId(Context ctx) {
|
||||
if (ctx.header("Authorization") != null) {
|
||||
String token = ctx.header("Authorization").replace("Bearer ", "");
|
||||
Claims claims = JwtUtils.parseJwt(token);
|
||||
if (claims != null) return claims.get("user_id").toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public class NoteController extends BaseController {
|
||||
@Db
|
||||
DbContext db;
|
||||
|
||||
private static final String QUERY_F = "id,pid,title,createtime,updatetime,viewtime,flag";
|
||||
private static final String QUERY_F = "id,pid,title,createtime,updatetime,viewtime,flag,tags";
|
||||
|
||||
@Get
|
||||
@Mapping("get1")
|
||||
@@ -64,7 +64,10 @@ public class NoteController extends BaseController {
|
||||
@Get
|
||||
@Mapping("/all")
|
||||
public Result noteAccess(Context ctx) throws Exception {
|
||||
List<NoteModel> all = ((DbTableQuery)this.db.table("note").whereEq("flag", Integer.valueOf(1))).selectList("id,pid,title,createtime,updatetime,viewtime,flag", NoteModel.class);
|
||||
String userId = getUserInfoId(ctx);
|
||||
DbTableQuery query = this.db.table("note").whereEq("flag", 1);
|
||||
if (userId != null) query = query.andEq("userid", Integer.valueOf(userId));
|
||||
List<NoteModel> all = query.selectList("id,pid,title,createtime,updatetime,viewtime,flag,tags", NoteModel.class);
|
||||
List<TreeNode> treeNodeList = new ArrayList<>();
|
||||
for (NoteModel n : all) {
|
||||
TreeNode treeNode = new TreeNode(n.getId().intValue(), n.getPid().intValue(), n.getTitle(), String.valueOf(n.getId()), String.valueOf(n.getId()));
|
||||
@@ -78,17 +81,29 @@ public class NoteController extends BaseController {
|
||||
@Get
|
||||
@Mapping("/home")
|
||||
public Result noteHome(Context ctx, @Path int type, @Path String search) throws Exception {
|
||||
String userId = getUserInfoId(ctx);
|
||||
List<NoteModel> all = new ArrayList<>();
|
||||
if (type == 0) {
|
||||
all = ((DbTableQuery)((DbTableQuery)this.db.table("note").whereLk("context", "%" + search + "%")).andLk("title", "%" + search + "%")).selectList("id,pid,title,createtime,updatetime,viewtime,flag", NoteModel.class);
|
||||
DbTableQuery q = this.db.table("note")
|
||||
.where("flag = 1 AND (title LIKE ? OR context LIKE ?)", "%" + search + "%", "%" + search + "%");
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
all = q.selectList("id,pid,title,createtime,updatetime,viewtime,flag,tags", NoteModel.class);
|
||||
} else if (type == 1) {
|
||||
all = ((DbTableQuery)((DbTableQuery)((DbTableQuery)this.db.table("note").whereEq("flag", Integer.valueOf(1))).orderByDesc("updatetime")).limit(20)).selectList("id,pid,title,createtime,updatetime,viewtime,flag", NoteModel.class);
|
||||
DbTableQuery q = this.db.table("note").whereEq("flag", 1);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
all = q.orderByDesc("updatetime").limit(20).selectList("id,pid,title,createtime,updatetime,viewtime,flag,tags", NoteModel.class);
|
||||
} else if (type == 2) {
|
||||
all = ((DbTableQuery)((DbTableQuery)((DbTableQuery)this.db.table("note").whereEq("flag", Integer.valueOf(1))).orderByDesc("createtime")).limit(20)).selectList("id,pid,title,createtime,updatetime,viewtime,flag", NoteModel.class);
|
||||
DbTableQuery q = this.db.table("note").whereEq("flag", 1);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
all = q.orderByDesc("createtime").limit(20).selectList("id,pid,title,createtime,updatetime,viewtime,flag,tags", NoteModel.class);
|
||||
} else if (type == 3) {
|
||||
all = ((DbTableQuery)((DbTableQuery)((DbTableQuery)this.db.table("note").whereEq("flag", Integer.valueOf(1))).orderByDesc("viewtime")).limit(20)).selectList("id,pid,title,createtime,updatetime,viewtime,flag", NoteModel.class);
|
||||
DbTableQuery q = this.db.table("note").whereEq("flag", 1);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
all = q.orderByDesc("viewtime").limit(20).selectList("id,pid,title,createtime,updatetime,viewtime,flag,tags", NoteModel.class);
|
||||
} else if (type == 4) {
|
||||
all = ((DbTableQuery)((DbTableQuery)((DbTableQuery)this.db.table("note").whereEq("flag", Integer.valueOf(0))).orderByDesc("updatetime")).limit(10000)).selectList("id,pid,title,createtime,updatetime,viewtime,flag", NoteModel.class);
|
||||
DbTableQuery q = this.db.table("note").whereEq("flag", 0);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
all = q.orderByDesc("updatetime").limit(10000).selectList("id,pid,title,createtime,updatetime,viewtime,flag,tags", NoteModel.class);
|
||||
}
|
||||
return Result.succeed(JSONArray.parseArray(JSONArray.toJSONString(all)));
|
||||
}
|
||||
@@ -96,15 +111,17 @@ public class NoteController extends BaseController {
|
||||
@Get
|
||||
@Mapping("/get")
|
||||
public Result getNote(Context ctx, @Path int id) throws Exception {
|
||||
NoteModel model = (NoteModel)((DbTableQuery)this.db.table("note").whereEq("id", Integer.valueOf(id))).selectItem("*", NoteModel.class);
|
||||
String userId = getUserInfoId(ctx);
|
||||
DbTableQuery q = this.db.table("note").whereEq("id", id);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
NoteModel model = q.selectItem("*", NoteModel.class);
|
||||
if (model == null) {
|
||||
ctx.status(404);
|
||||
return Result.failure(404, "笔记不存在");
|
||||
}
|
||||
String time = DateUtil.now();
|
||||
((DbTableQuery)this.db.table("note").set("viewtime", time).whereEq("id", Integer.valueOf(id))).update();
|
||||
this.db.table("note").set("viewtime", time).whereEq("id", id).update();
|
||||
model.setViewtime(time);
|
||||
log.info(JSONObject.toJSONString(model, new com.alibaba.fastjson2.JSONWriter.Feature[0]));
|
||||
return Result.succeed(model);
|
||||
}
|
||||
|
||||
@@ -121,9 +138,8 @@ public class NoteController extends BaseController {
|
||||
note.setCreatetime(DateUtil.now());
|
||||
note.setUpdatetime(note.getCreatetime());
|
||||
note.setViewtime(note.getCreatetime());
|
||||
note.setUserid(Integer.valueOf(userid));
|
||||
note.setContext("{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"未命名Note\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}");
|
||||
|
||||
// note.setContext("{\"root\": {\"type\": \"root\", \"format\": \"\", \"indent\": 0, \"version\": 1, \"children\": [{\"type\": \"paragraph\", \"format\": \"\", \"indent\": 0, \"version\": 1, \"children\": [], \"direction\": null}], \"direction\": null}}");
|
||||
this.db.table("note").setEntityIf(note, (k, v) -> Boolean.valueOf((v != null))).insert();
|
||||
return Result.succeed("ok");
|
||||
}
|
||||
@@ -135,26 +151,32 @@ public class NoteController extends BaseController {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "参数不完整");
|
||||
}
|
||||
NoteModel note = (NoteModel)((DbTableQuery)this.db.table("note").whereEq("id", noteRequest.getId())).selectItem("id,pid,title,createtime,updatetime,viewtime,flag", NoteModel.class);
|
||||
String userId = getUserInfoId(ctx);
|
||||
DbTableQuery q = this.db.table("note").whereEq("id", noteRequest.getId());
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
NoteModel note = q.selectItem("id,pid,title,createtime,updatetime,viewtime,flag,tags", NoteModel.class);
|
||||
if (note == null) {
|
||||
ctx.status(404);
|
||||
return Result.failure(404, "笔记不存在");
|
||||
}
|
||||
note.setTitle(noteRequest.getTitle());
|
||||
note.setUpdatetime(DateUtil.now());
|
||||
String cc = noteRequest.getContext();
|
||||
JSONObject ar = JSONObject.parseObject(cc);
|
||||
note.setContext(ar.toJSONString());
|
||||
int flag = ((DbTableQuery)this.db.table("note").set("title", noteRequest.getTitle()).set("updatetime", DateUtil.now()).set("context", ar.toJSONString()).whereEq("id", noteRequest.getId())).update();
|
||||
log.info("updateNote+ flag");
|
||||
HistoryModel history = new HistoryModel();
|
||||
DbTableQuery uq = this.db.table("note")
|
||||
.set("title", noteRequest.getTitle())
|
||||
.set("updatetime", DateUtil.now())
|
||||
.set("context", ar.toJSONString())
|
||||
.set("tags", noteRequest.getTags() != null ? noteRequest.getTags() : "")
|
||||
.whereEq("id", noteRequest.getId());
|
||||
if (userId != null) uq = uq.andEq("userid", Integer.valueOf(userId));
|
||||
uq.update();
|
||||
log.info("updateNote id=" + noteRequest.getId());
|
||||
HistoryModel history = new HistoryModel();
|
||||
history.setNid(Integer.valueOf(noteRequest.getId().intValue()));
|
||||
history.setTitle(noteRequest.getTitle());
|
||||
history.setFlag(Integer.valueOf(1));
|
||||
history.setCreatetime(DateUtil.now());
|
||||
history.setContext(ar.toJSONString());
|
||||
long his = this.db.table("history").setEntityIf(history, (k, v) -> Boolean.valueOf((v != null))).insert();
|
||||
log.info("history+ his");
|
||||
this.db.table("history").setEntityIf(history, (k, v) -> Boolean.valueOf((v != null))).insert();
|
||||
return Result.succeed("ok");
|
||||
}
|
||||
|
||||
@@ -165,7 +187,10 @@ public class NoteController extends BaseController {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "笔记ID不能为空");
|
||||
}
|
||||
int flag = ((DbTableQuery)this.db.table("note").set("flag", Integer.valueOf(0)).set("updatetime", DateUtil.now()).whereEq("id", noteRequest.getId())).update();
|
||||
String userId = getUserInfoId(ctx);
|
||||
DbTableQuery q = this.db.table("note").set("flag", 0).set("updatetime", DateUtil.now()).whereEq("id", noteRequest.getId());
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
int flag = q.update();
|
||||
if (flag == 0) {
|
||||
ctx.status(404);
|
||||
return Result.failure(404, "笔记不存在");
|
||||
@@ -176,7 +201,28 @@ public class NoteController extends BaseController {
|
||||
@Get
|
||||
@Mapping("/deleteBack")
|
||||
public Result deleteBack(Context ctx, @Path int id) throws Exception {
|
||||
int flag = ((DbTableQuery)this.db.table("note").set("flag", Integer.valueOf(1)).set("updatetime", DateUtil.now()).whereEq("id", Integer.valueOf(id))).update();
|
||||
String userId = getUserInfoId(ctx);
|
||||
DbTableQuery q = this.db.table("note").set("flag", 1).set("updatetime", DateUtil.now()).whereEq("id", id);
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
int flag = q.update();
|
||||
if (flag == 0) {
|
||||
ctx.status(404);
|
||||
return Result.failure(404, "笔记不存在");
|
||||
}
|
||||
return Result.succeed("ok");
|
||||
}
|
||||
|
||||
@Post
|
||||
@Mapping("/move")
|
||||
public Result moveNote(Context ctx, @Body NoteRequest noteRequest) throws SQLException {
|
||||
if (noteRequest == null || noteRequest.getId() == null || noteRequest.getPid() == null) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "参数不完整");
|
||||
}
|
||||
String userId = getUserInfoId(ctx);
|
||||
DbTableQuery q = this.db.table("note").set("pid", noteRequest.getPid()).set("updatetime", DateUtil.now()).whereEq("id", noteRequest.getId());
|
||||
if (userId != null) q = q.andEq("userid", Integer.valueOf(userId));
|
||||
int flag = q.update();
|
||||
if (flag == 0) {
|
||||
ctx.status(404);
|
||||
return Result.failure(404, "笔记不存在");
|
||||
@@ -187,21 +233,52 @@ public class NoteController extends BaseController {
|
||||
@Get
|
||||
@Mapping("/historyQueryOrDelete")
|
||||
public Result historyQueryOrDelete(@Path int flag) throws Exception {
|
||||
|
||||
log.info("history+ flag" + flag);
|
||||
|
||||
if(flag==0){
|
||||
if (flag == 0) {
|
||||
long count = db.table("history").selectCount();
|
||||
log.info("count:" + count);
|
||||
return Result.succeed(count);
|
||||
}else if(flag==1){
|
||||
int flagdb = db.table("history").whereEq("flag",1).delete();
|
||||
} else if (flag == 1) {
|
||||
int flagdb = db.table("history").whereEq("flag", 1).delete();
|
||||
log.info("flagdb:" + flagdb);
|
||||
return Result.succeed(0);
|
||||
}
|
||||
return Result.failure(400, "不支持的操作类型");
|
||||
}
|
||||
|
||||
@Get
|
||||
@Mapping("/history")
|
||||
public Result getNoteHistory(Context ctx, @Path int nid) throws Exception {
|
||||
if (nid <= 0) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "笔记ID不能为空");
|
||||
}
|
||||
List<HistoryModel> list = this.db.table("history")
|
||||
.whereEq("nid", nid)
|
||||
.whereEq("flag", 1)
|
||||
.orderByDesc("createtime")
|
||||
.limit(50)
|
||||
.selectList("id,nid,title,createtime", HistoryModel.class);
|
||||
return Result.succeed(JSONArray.parseArray(JSONArray.toJSONString(list)));
|
||||
}
|
||||
|
||||
@Get
|
||||
@Mapping("/history/content")
|
||||
public Result getHistoryContent(Context ctx, @Path int id) throws Exception {
|
||||
if (id <= 0) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "历史ID不能为空");
|
||||
}
|
||||
HistoryModel model = this.db.table("history")
|
||||
.whereEq("id", id)
|
||||
.selectItem("*", HistoryModel.class);
|
||||
if (model == null) {
|
||||
ctx.status(404);
|
||||
return Result.failure(404, "历史记录不存在");
|
||||
}
|
||||
return Result.succeed(model);
|
||||
}
|
||||
|
||||
private String getUserInfoId(Context ctx) {
|
||||
String userId = "";
|
||||
if (null != ctx.header("Authorization")) {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.cyynote.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
import org.noear.solon.annotation.Body;
|
||||
import org.noear.solon.annotation.Controller;
|
||||
import org.noear.solon.annotation.Get;
|
||||
import org.noear.solon.annotation.Mapping;
|
||||
import org.noear.solon.annotation.Post;
|
||||
import org.noear.solon.core.handle.Context;
|
||||
import org.noear.solon.core.handle.Result;
|
||||
import org.noear.wood.DbContext;
|
||||
import org.noear.wood.DbTableQuery;
|
||||
import org.noear.wood.DataItem;
|
||||
import org.noear.wood.annotation.Db;
|
||||
|
||||
@Mapping("/api/settings")
|
||||
@Controller
|
||||
public class SettingsController extends BaseController {
|
||||
private static final Logger log = Logger.getLogger(SettingsController.class.getName());
|
||||
|
||||
@Db
|
||||
DbContext db;
|
||||
|
||||
// Allowed setting keys (whitelist for security)
|
||||
private static final List<String> ALLOWED_KEYS = List.of(
|
||||
"r2_endpoint", "r2_access_key_id", "r2_secret_access_key", "r2_bucket_name",
|
||||
"r2_public_url", "app_theme", "local_storage_path"
|
||||
);
|
||||
|
||||
@Get
|
||||
@Mapping("/get/{key}")
|
||||
public Result getSetting(Context ctx, String key) throws SQLException {
|
||||
if (!ALLOWED_KEYS.contains(key)) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "不支持的配置项");
|
||||
}
|
||||
DataItem row = (DataItem) this.db.table("settings").whereEq("key", key).selectItem("key,value", DataItem.class);
|
||||
if (row == null) return Result.succeed(null);
|
||||
String value = row.getString("value");
|
||||
if ("r2_secret_access_key".equals(key) && value != null && value.length() > 4) {
|
||||
value = "****" + value.substring(value.length() - 4);
|
||||
}
|
||||
return Result.succeed(value);
|
||||
}
|
||||
|
||||
@Get
|
||||
@Mapping("/all")
|
||||
public Result getAllSettings(Context ctx) throws SQLException {
|
||||
JSONObject result = new JSONObject();
|
||||
for (String key : ALLOWED_KEYS) {
|
||||
DataItem row = (DataItem) this.db.table("settings").whereEq("key", key).selectItem("key,value", DataItem.class);
|
||||
String value = row == null ? null : row.getString("value");
|
||||
if ("r2_secret_access_key".equals(key) && value != null && value.length() > 4) {
|
||||
value = "****" + value.substring(value.length() - 4);
|
||||
}
|
||||
result.put(key, value);
|
||||
}
|
||||
return Result.succeed(result);
|
||||
}
|
||||
|
||||
@Post
|
||||
@Mapping("/set")
|
||||
public Result setSetting(Context ctx, @Body JSONObject body) throws SQLException {
|
||||
if (body == null) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "参数不能为空");
|
||||
}
|
||||
for (String key : body.keySet()) {
|
||||
if (!ALLOWED_KEYS.contains(key)) {
|
||||
ctx.status(400);
|
||||
return Result.failure(400, "不支持的配置项: " + key);
|
||||
}
|
||||
String value = body.getString(key);
|
||||
long exists = this.db.table("settings").whereEq("key", key).selectCount();
|
||||
if (exists > 0) {
|
||||
this.db.table("settings").set("value", value).whereEq("key", key).update();
|
||||
} else {
|
||||
this.db.table("settings").set("key", key).set("value", value).insert();
|
||||
}
|
||||
}
|
||||
return Result.succeed("ok");
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,174 @@
|
||||
package com.cyynote.dso;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
public class DsHelper {
|
||||
private static final Logger log = Logger.getLogger(DsHelper.class.getName());
|
||||
private static boolean inited = false;
|
||||
|
||||
public static void initData(DataSource ds) {
|
||||
if (inited)
|
||||
return;
|
||||
if (inited) return;
|
||||
inited = true;
|
||||
Connection conn = null;
|
||||
try {
|
||||
conn = ds.getConnection();
|
||||
PreparedStatement ps = conn.prepareStatement("SELECT name FROM sqlite_master WHERE type='table' AND name='appx';");
|
||||
ResultSet res = ps.executeQuery();
|
||||
if (!res.next()) {
|
||||
String[] sqls = getSqlFromFile();
|
||||
for (String sql : sqls)
|
||||
runSql(conn, sql);
|
||||
try (Connection conn = ds.getConnection()) {
|
||||
// 1. Initial schema: create all base tables if not present
|
||||
if (!tableExists(conn, "appx")) {
|
||||
log.info("[DB] Running initial schema...");
|
||||
for (String sql : loadSqlFile("/db/schema.sql")) {
|
||||
runSqlIgnoreError(conn, sql, null);
|
||||
}
|
||||
}
|
||||
ps.close();
|
||||
} catch (SQLException sqlException) {
|
||||
throw new RuntimeException(sqlException);
|
||||
} finally {
|
||||
try {
|
||||
if (conn != null)
|
||||
conn.close();
|
||||
} catch (SQLException sQLException) {}
|
||||
// 2. Versioned migrations
|
||||
runMigrations(conn);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB init failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] getSqlFromFile() {
|
||||
private static void runMigrations(Connection conn) throws SQLException {
|
||||
runSql(conn, "CREATE TABLE IF NOT EXISTS db_version (" +
|
||||
"version INTEGER PRIMARY KEY, applied_at TEXT)");
|
||||
|
||||
List<String> migrationFiles = listMigrationFiles();
|
||||
Collections.sort(migrationFiles);
|
||||
|
||||
for (String fileName : migrationFiles) {
|
||||
int version = parseMigrationVersion(fileName);
|
||||
if (version < 0) continue;
|
||||
if (isMigrationApplied(conn, version)) continue;
|
||||
|
||||
log.info("[DB] Running migration: " + fileName);
|
||||
String[] statements = loadSqlFile("/db/migrations/" + fileName);
|
||||
for (String sql : statements) {
|
||||
runSqlIgnoreError(conn, sql, "duplicate column");
|
||||
}
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"INSERT OR IGNORE INTO db_version(version, applied_at) VALUES(?, datetime('now'))");
|
||||
ps.setInt(1, version);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
log.info("[DB] Migration V" + version + " applied.");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMigrationApplied(Connection conn, int version) throws SQLException {
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT 1 FROM db_version WHERE version = ?");
|
||||
ps.setInt(1, version);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
boolean exists = rs.next();
|
||||
ps.close();
|
||||
return exists;
|
||||
}
|
||||
|
||||
private static List<String> listMigrationFiles() {
|
||||
List<String> files = new ArrayList<>();
|
||||
try {
|
||||
InputStream ins = com.cyynote.dso.DsHelper.class.getResourceAsStream("/db/schema.sql");
|
||||
int len = ins.available();
|
||||
byte[] bs = new byte[len];
|
||||
ins.read(bs);
|
||||
String str = new String(bs, "UTF-8");
|
||||
String[] sql = str.split(";");
|
||||
return sql;
|
||||
URL dirUrl = DsHelper.class.getResource("/db/migrations/");
|
||||
if (dirUrl == null) return files;
|
||||
String protocol = dirUrl.getProtocol();
|
||||
if ("file".equals(protocol)) {
|
||||
java.io.File dir = new java.io.File(dirUrl.toURI());
|
||||
if (dir.isDirectory()) {
|
||||
for (java.io.File f : dir.listFiles()) {
|
||||
if (f.getName().startsWith("V") && f.getName().endsWith(".sql")) {
|
||||
files.add(f.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ("jar".equals(protocol)) {
|
||||
String jarPath = dirUrl.getPath();
|
||||
String jarFile = jarPath.substring(5, jarPath.indexOf("!"));
|
||||
String prefix = jarPath.substring(jarPath.indexOf("!") + 2);
|
||||
try (java.util.jar.JarFile jar = new java.util.jar.JarFile(jarFile)) {
|
||||
java.util.Enumeration<java.util.jar.JarEntry> entries = jar.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
String name = entries.nextElement().getName();
|
||||
if (name.startsWith(prefix) && name.endsWith(".sql")) {
|
||||
String fileName = name.substring(name.lastIndexOf('/') + 1);
|
||||
if (fileName.startsWith("V")) {
|
||||
files.add(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warning("[DB] Could not list migration files: " + e.getMessage());
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
private static int parseMigrationVersion(String fileName) {
|
||||
try {
|
||||
String num = fileName.substring(1, fileName.indexOf("__"));
|
||||
return Integer.parseInt(num);
|
||||
} catch (Exception e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean tableExists(Connection conn, String tableName) throws SQLException {
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name=?");
|
||||
ps.setString(1, tableName);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
boolean exists = rs.next();
|
||||
ps.close();
|
||||
return exists;
|
||||
}
|
||||
|
||||
private static String[] loadSqlFile(String path) {
|
||||
try {
|
||||
InputStream ins = DsHelper.class.getResourceAsStream(path);
|
||||
if (ins == null) return new String[0];
|
||||
byte[] bs = ins.readAllBytes();
|
||||
String str = new String(bs, StandardCharsets.UTF_8);
|
||||
String[] parts = str.split(";");
|
||||
List<String> result = new ArrayList<>();
|
||||
for (String p : parts) {
|
||||
String trimmed = p.trim();
|
||||
if (!trimmed.isEmpty()) result.add(trimmed);
|
||||
}
|
||||
return result.toArray(new String[0]);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
throw new RuntimeException("Failed to load SQL file: " + path, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void runSql(Connection conn, String sql) throws SQLException {
|
||||
System.out.println(sql);
|
||||
PreparedStatement ps = conn.prepareStatement(sql);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
Statement st = conn.createStatement();
|
||||
st.execute(sql);
|
||||
st.close();
|
||||
}
|
||||
|
||||
private static void runSqlIgnoreError(Connection conn, String sql, String ignoreContaining) {
|
||||
try {
|
||||
String trimmed = sql.trim();
|
||||
if (trimmed.isEmpty()) return;
|
||||
log.info("[DB SQL] " + trimmed.substring(0, Math.min(80, trimmed.length())));
|
||||
Statement st = conn.createStatement();
|
||||
st.execute(trimmed);
|
||||
st.close();
|
||||
} catch (SQLException e) {
|
||||
if (ignoreContaining != null && e.getMessage() != null &&
|
||||
e.getMessage().toLowerCase().contains(ignoreContaining.toLowerCase())) {
|
||||
log.info("[DB] Ignored: " + e.getMessage());
|
||||
} else {
|
||||
log.warning("[DB] SQL error (non-fatal): " + e.getMessage() + " | SQL: " + sql.trim().substring(0, Math.min(80, sql.trim().length())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.cyynote.model;
|
||||
|
||||
import lombok.Data;
|
||||
import org.noear.wood.annotation.PrimaryKey;
|
||||
import org.noear.wood.annotation.Table;
|
||||
|
||||
@Data
|
||||
@Table("drive_file")
|
||||
public class DriveFileModel {
|
||||
@PrimaryKey
|
||||
public Integer id;
|
||||
public String filename;
|
||||
public String originalName;
|
||||
public Long size;
|
||||
public String mimeType;
|
||||
public String r2Key;
|
||||
public String shareToken;
|
||||
public String shareExpiry;
|
||||
public Integer userid;
|
||||
public String createtime;
|
||||
public Integer flag;
|
||||
public String storageMode;
|
||||
public String localPath;
|
||||
|
||||
public void setId(Integer id) { this.id = id; }
|
||||
public Integer getId() { return id; }
|
||||
|
||||
public void setFilename(String filename) { this.filename = filename; }
|
||||
public String getFilename() { return filename; }
|
||||
|
||||
public void setOriginalName(String originalName) { this.originalName = originalName; }
|
||||
public String getOriginalName() { return originalName; }
|
||||
|
||||
public void setSize(Long size) { this.size = size; }
|
||||
public Long getSize() { return size; }
|
||||
|
||||
public void setMimeType(String mimeType) { this.mimeType = mimeType; }
|
||||
public String getMimeType() { return mimeType; }
|
||||
|
||||
public void setR2Key(String r2Key) { this.r2Key = r2Key; }
|
||||
public String getR2Key() { return r2Key; }
|
||||
|
||||
public void setShareToken(String shareToken) { this.shareToken = shareToken; }
|
||||
public String getShareToken() { return shareToken; }
|
||||
|
||||
public void setShareExpiry(String shareExpiry) { this.shareExpiry = shareExpiry; }
|
||||
public String getShareExpiry() { return shareExpiry; }
|
||||
|
||||
public void setUserid(Integer userid) { this.userid = userid; }
|
||||
public Integer getUserid() { return userid; }
|
||||
|
||||
public void setCreatetime(String createtime) { this.createtime = createtime; }
|
||||
public String getCreatetime() { return createtime; }
|
||||
|
||||
public void setFlag(Integer flag) { this.flag = flag; }
|
||||
public Integer getFlag() { return flag; }
|
||||
|
||||
public void setStorageMode(String storageMode) { this.storageMode = storageMode; }
|
||||
public String getStorageMode() { return storageMode; }
|
||||
|
||||
public void setLocalPath(String localPath) { this.localPath = localPath; }
|
||||
public String getLocalPath() { return localPath; }
|
||||
}
|
||||
@@ -25,8 +25,12 @@ public class NoteModel {
|
||||
|
||||
public String viewtime;
|
||||
|
||||
public Integer userid;
|
||||
|
||||
public Integer flag;
|
||||
|
||||
public String tags;
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
@@ -63,6 +67,14 @@ public class NoteModel {
|
||||
this.flag = flag;
|
||||
}
|
||||
|
||||
public void setUserid(Integer userid) {
|
||||
this.userid = userid;
|
||||
}
|
||||
|
||||
public Integer getUserid() {
|
||||
return this.userid;
|
||||
}
|
||||
|
||||
|
||||
public String toString() {
|
||||
return "NoteModel(id=" + getId() + ", pid=" + getPid() + ", title=" + getTitle() + ", context=" + getContext() + ", conjson=" + getConjson() + ", createtime=" + getCreatetime() + ", updatetime=" + getUpdatetime() + ", viewtime=" + getViewtime() + ", flag=" + getFlag() + ")";
|
||||
@@ -103,5 +115,13 @@ public class NoteModel {
|
||||
public Integer getFlag() {
|
||||
return this.flag;
|
||||
}
|
||||
|
||||
public void setTags(String tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
public String getTags() {
|
||||
return this.tags;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ public class UserModel {
|
||||
|
||||
public String logintime;
|
||||
|
||||
public Integer passwordHashed;
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
@@ -51,6 +53,14 @@ public class UserModel {
|
||||
this.logintime = logintime;
|
||||
}
|
||||
|
||||
public Integer getPasswordHashed() {
|
||||
return this.passwordHashed;
|
||||
}
|
||||
|
||||
public void setPasswordHashed(Integer passwordHashed) {
|
||||
this.passwordHashed = passwordHashed;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "UserModel(id=" + getId() + ", email=" + getEmail() + ", password=" + getPassword() + ", username=" + getUsername() + ", role=" + getRole() + ", token=" + getToken() + ", logintime=" + getLogintime() + ")";
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ public class NoteRequest {
|
||||
|
||||
private String context;
|
||||
|
||||
private String tags;
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
@@ -41,4 +43,12 @@ public class NoteRequest {
|
||||
public void setContext(String context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public String getTags() {
|
||||
return this.tags;
|
||||
}
|
||||
|
||||
public void setTags(String tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS drive_file (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
filename TEXT,
|
||||
original_name TEXT,
|
||||
size INTEGER,
|
||||
mime_type TEXT,
|
||||
r2_key TEXT,
|
||||
share_token TEXT,
|
||||
share_expiry TEXT,
|
||||
userid INTEGER,
|
||||
createtime TEXT,
|
||||
flag INTEGER DEFAULT 1
|
||||
);
|
||||
|
||||
ALTER TABLE user ADD COLUMN password_hashed INTEGER DEFAULT 0;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Ensure note.userid column exists (for older databases created before this field was added)
|
||||
ALTER TABLE note ADD COLUMN userid INTEGER DEFAULT 0;
|
||||
|
||||
-- Index for faster per-user queries
|
||||
CREATE INDEX IF NOT EXISTS idx_note_userid ON note(userid);
|
||||
CREATE INDEX IF NOT EXISTS idx_note_flag_userid ON note(flag, userid);
|
||||
@@ -0,0 +1,5 @@
|
||||
-- V4: Assign existing notes without an owner to userid=1 (first admin)
|
||||
-- This ensures production data created before multi-user isolation is not lost.
|
||||
UPDATE note SET userid = 1 WHERE userid IS NULL OR userid = 0;
|
||||
|
||||
INSERT OR IGNORE INTO db_version (version, applied_at) VALUES (4, datetime('now'));
|
||||
@@ -0,0 +1,4 @@
|
||||
-- V5: Add tags column to note table
|
||||
ALTER TABLE note ADD COLUMN tags TEXT DEFAULT '';
|
||||
|
||||
INSERT OR IGNORE INTO db_version (version, applied_at) VALUES (5, datetime('now'));
|
||||
@@ -0,0 +1,5 @@
|
||||
-- V6: Add local storage support for drive files
|
||||
ALTER TABLE drive_file ADD COLUMN storage_mode TEXT DEFAULT 'r2';
|
||||
ALTER TABLE drive_file ADD COLUMN local_path TEXT;
|
||||
|
||||
INSERT OR IGNORE INTO db_version (version, applied_at) VALUES (6, datetime('now'));
|
||||
Reference in New Issue
Block a user