Add storage

This commit is contained in:
2026-05-15 19:01:10 +08:00
parent 8a16b01e9e
commit 8aa7387e4d
49 changed files with 2112 additions and 223 deletions
Binary file not shown.
+7
View File
@@ -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'));
@@ -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
@@ -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,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