Add storage
This commit is contained in:
Binary file not shown.
@@ -84,6 +84,13 @@
|
||||
<artifactId>solon-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- AWS S3 SDK v2 (Cloudflare R2 is S3-compatible) -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
<version>2.25.16</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
|
||||
@@ -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'));
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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'));
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
#Generated by Maven
|
||||
#Thu May 14 15:10:41 CST 2026
|
||||
#Fri May 15 17:58:56 CST 2026
|
||||
groupId=com.cyynote
|
||||
artifactId=cyynote
|
||||
version=1.0.0
|
||||
|
||||
+3
@@ -1,5 +1,6 @@
|
||||
com/cyynote/payload/request/LoginRequest.class
|
||||
com/cyynote/dso/DsHelper.class
|
||||
com/cyynote/controller/SettingsController.class
|
||||
com/cyynote/controller/JwtInterceptor.class
|
||||
com/cyynote/util/TreeNode.class
|
||||
com/cyynote/model/Appx.class
|
||||
@@ -14,12 +15,14 @@ com/cyynote/Config.class
|
||||
com/cyynote/payload/request/UpdatePasswordRequest.class
|
||||
com/cyynote/payload/request/NoteRequest.class
|
||||
com/cyynote/WebApp.class
|
||||
com/cyynote/controller/DriveController.class
|
||||
com/cyynote/dso/AuthSqlAnnotation.class
|
||||
com/cyynote/dso/SqlMapper.class
|
||||
com/cyynote/payload/response/JwtResponse.class
|
||||
com/cyynote/controller/AuthController.class
|
||||
com/cyynote/dso/NoteSqlAnnotation.class
|
||||
com/cyynote/model/AppxModel.class
|
||||
com/cyynote/model/DriveFileModel.class
|
||||
com/cyynote/dso/SqlAnnotation.class
|
||||
com/cyynote/controller/BaseController.class
|
||||
com/cyynote/util/TreeBuild.class
|
||||
|
||||
+3
@@ -3,8 +3,10 @@
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/AuthController.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/BaseController.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/DemoController.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/DriveController.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/JwtInterceptor.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/NoteController.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/SettingsController.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/Test2Controller.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/AuthProcessorImpl.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/AuthSqlAnnotation.java
|
||||
@@ -14,6 +16,7 @@
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/SqlMapper.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/Appx.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/AppxModel.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/DriveFileModel.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/HistoryModel.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/NoteModel.java
|
||||
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/UserModel.java
|
||||
|
||||
Reference in New Issue
Block a user