🎉
This commit is contained in:
parent
6109b6aea4
commit
098baa995a
12
pom.xml
12
pom.xml
@ -32,12 +32,6 @@
|
|||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
|
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
|
||||||
</properties>
|
</properties>
|
||||||
<repositories>
|
|
||||||
<repository>
|
|
||||||
<id>jitpack.io</id>
|
|
||||||
<url>https://jitpack.io</url>
|
|
||||||
</repository>
|
|
||||||
</repositories>
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
@ -59,12 +53,6 @@
|
|||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- <dependency>-->
|
|
||||||
<!-- <groupId>com.github.wb04307201</groupId>-->
|
|
||||||
<!-- <artifactId>file-preview-spring-boot-starter</artifactId>-->
|
|
||||||
<!-- <version>1.2.10</version>-->
|
|
||||||
<!-- </dependency>-->
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
package vin.vio.collaboraonline;
|
package vin.vio.collaboraonline;
|
||||||
|
|
||||||
import cn.wubo.file.preview.EnableFilePreview;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.web.filter.CommonsRequestLoggingFilter;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class CollaboraOnlineApplication {
|
public class CollaboraOnlineApplication {
|
||||||
@ -10,5 +11,4 @@ public class CollaboraOnlineApplication {
|
|||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(CollaboraOnlineApplication.class, args);
|
SpringApplication.run(CollaboraOnlineApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,115 @@
|
|||||||
|
package vin.vio.collaboraonline.controller;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import vin.vio.collaboraonline.model.WopiFile;
|
||||||
|
import vin.vio.collaboraonline.service.FileService;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: TODO
|
||||||
|
* @Date: 2025/2/19 16:49
|
||||||
|
* @Created: by ZZSLL
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/wopi")
|
||||||
|
public class WopiController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FileService fileService;
|
||||||
|
|
||||||
|
// 检查文件信息
|
||||||
|
@GetMapping("/files/{fileId}")
|
||||||
|
public ResponseEntity<?> checkFileInfo(@PathVariable String fileId) {
|
||||||
|
|
||||||
|
WopiFile file = fileService.getFileInfo(fileId);
|
||||||
|
if (file == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(createCheckFileInfoResponse(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件内容
|
||||||
|
@GetMapping("/files/{fileId}/contents")
|
||||||
|
public ResponseEntity<Resource> getFile(@PathVariable String fileId) {
|
||||||
|
|
||||||
|
org.springframework.core.io.Resource fileResource = fileService.loadFileAsResource(fileId);
|
||||||
|
if (fileResource == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header("Content-Type", "application/octet-stream")
|
||||||
|
.body(fileResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
@RequestMapping(value = "/files/{fileId}/contents", method = {RequestMethod.POST, RequestMethod.PUT})
|
||||||
|
public ResponseEntity<Void> putFile(
|
||||||
|
@PathVariable String fileId, InputStream contentStream) {
|
||||||
|
|
||||||
|
boolean success = fileService.saveFile(fileId, contentStream);
|
||||||
|
return success ?
|
||||||
|
ResponseEntity.ok().build() :
|
||||||
|
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> createCheckFileInfoResponse(WopiFile file) {
|
||||||
|
Map<String, Object> response = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// 强制开启所有编辑权限
|
||||||
|
response.put("BaseFileName", file.getFileName());
|
||||||
|
response.put("Size", file.getSize());
|
||||||
|
response.put("OwnerId", "test_admin"); // 固定测试账户
|
||||||
|
response.put("UserId", "test_user"); // 固定测试用户
|
||||||
|
|
||||||
|
// 关键权限开关组
|
||||||
|
response.put("UserCanWrite", true); // 强制可写
|
||||||
|
response.put("UserCanNotWriteRelative", false);
|
||||||
|
response.put("SupportsLocks", false); // 禁用文件锁检测
|
||||||
|
response.put("SupportsUpdate", true);
|
||||||
|
response.put("EnableOwnerTermination", true); // 允许强制终止锁
|
||||||
|
|
||||||
|
// 关闭所有安全限制
|
||||||
|
response.put("IsAnonymousUser", true); // 允许匿名访问
|
||||||
|
response.put("ProtectInClient", false); // 禁用客户端保护
|
||||||
|
response.put("DisablePrint", false);
|
||||||
|
response.put("DisableExport", false);
|
||||||
|
response.put("DisableCopy", false);
|
||||||
|
|
||||||
|
// 添加必要协议字段
|
||||||
|
response.put("HostEditUrl", "http://192.168.3.104:8080/wopi/files/"+file.getFileName());
|
||||||
|
response.put("PostMessageOrigin", "http://192.168.3.104:8080"); // 本地测试用
|
||||||
|
|
||||||
|
// 测试环境特殊配置
|
||||||
|
response.put("BreadcrumbBrandName", "DEV ENV");
|
||||||
|
response.put("BreadcrumbBrandUrl", "http://192.168.3.104:8080");
|
||||||
|
response.put("BreadcrumbDocName", "[测试模式] "+file.getFileName());
|
||||||
|
|
||||||
|
// 绕过版本检查
|
||||||
|
// response.put("LastModifiedTime", "2020-01-01T00:00:00Z");
|
||||||
|
// response.put("Version", "test_version");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
100
src/main/java/vin/vio/collaboraonline/model/WopiFile.java
Normal file
100
src/main/java/vin/vio/collaboraonline/model/WopiFile.java
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package vin.vio.collaboraonline.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: TODO
|
||||||
|
* @Date: 2025/2/19 16:56
|
||||||
|
* @Created: by ZZSLL
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class WopiFile {
|
||||||
|
private String fileId;
|
||||||
|
private String fileName;
|
||||||
|
private long size;
|
||||||
|
private String ownerId;
|
||||||
|
private Path filePath;
|
||||||
|
private boolean userCanWrite;
|
||||||
|
private boolean supportsLocks;
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
|
public WopiFile() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public WopiFile(String fileId, String fileName, long size, String ownerId, Path filePath, boolean userCanWrite, boolean supportsLocks, String contentType) {
|
||||||
|
this.fileId = fileId;
|
||||||
|
this.fileName = fileName;
|
||||||
|
this.size = size;
|
||||||
|
this.ownerId = ownerId;
|
||||||
|
this.filePath = filePath;
|
||||||
|
this.userCanWrite = userCanWrite;
|
||||||
|
this.supportsLocks = supportsLocks;
|
||||||
|
this.contentType = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileId() {
|
||||||
|
return fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFileId(String fileId) {
|
||||||
|
this.fileId = fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileName() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFileName(String fileName) {
|
||||||
|
this.fileName = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSize(long size) {
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOwnerId() {
|
||||||
|
return ownerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOwnerId(String ownerId) {
|
||||||
|
this.ownerId = ownerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getFilePath() {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFilePath(Path filePath) {
|
||||||
|
this.filePath = filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUserCanWrite() {
|
||||||
|
return userCanWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserCanWrite(boolean userCanWrite) {
|
||||||
|
this.userCanWrite = userCanWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSupportsLocks() {
|
||||||
|
return supportsLocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSupportsLocks(boolean supportsLocks) {
|
||||||
|
this.supportsLocks = supportsLocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentType() {
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContentType(String contentType) {
|
||||||
|
this.contentType = contentType;
|
||||||
|
}
|
||||||
|
}
|
132
src/main/java/vin/vio/collaboraonline/service/FileService.java
Normal file
132
src/main/java/vin/vio/collaboraonline/service/FileService.java
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package vin.vio.collaboraonline.service;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
import org.springframework.core.io.UrlResource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import vin.vio.collaboraonline.model.WopiFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Classname FileService
|
||||||
|
* @Description TODO
|
||||||
|
* @Date 2025/2/19 16:54
|
||||||
|
* @Created by violet
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class FileService {
|
||||||
|
|
||||||
|
|
||||||
|
private static final String BASE_DIR = "E:\\Code\\CollaboraOnline\\files";
|
||||||
|
|
||||||
|
|
||||||
|
public WopiFile getFileInfo(String fileId) {
|
||||||
|
Path filePath = Paths.get(BASE_DIR, fileId);
|
||||||
|
|
||||||
|
File targetFile = filePath.toFile();
|
||||||
|
|
||||||
|
if (!targetFile.exists() || !targetFile.canRead() || !targetFile.canWrite()) {
|
||||||
|
throw new RuntimeException("File not accessible: " + filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
WopiFile file = new WopiFile();
|
||||||
|
try {
|
||||||
|
file.setFileId(fileId);
|
||||||
|
file.setFileName(targetFile.getName());
|
||||||
|
file.setSize(targetFile.length());
|
||||||
|
file.setFilePath(filePath);
|
||||||
|
|
||||||
|
file.setOwnerId("admin");
|
||||||
|
file.setUserCanWrite(true);
|
||||||
|
file.setSupportsLocks(true);
|
||||||
|
|
||||||
|
String contentType = Files.probeContentType(filePath);
|
||||||
|
file.setContentType(contentType != null ? contentType : "application/octet-stream");
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Error reading file metadata: " + filePath, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Resource loadFileAsResource(String fileId) {
|
||||||
|
try {
|
||||||
|
Path resolvedPath = validateFilePath(fileId);
|
||||||
|
|
||||||
|
FileSystemResource resource = new FileSystemResource(resolvedPath);
|
||||||
|
|
||||||
|
if (resource.exists() && resource.isReadable() && Files.isRegularFile(resolvedPath)) {
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (InvalidPathException | SecurityException e) {
|
||||||
|
throw new RuntimeException("非法文件访问: " + fileId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean saveFile(String fileId, InputStream content) {
|
||||||
|
Path targetPath = validateFilePath(fileId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Path tempFile = Files.createTempFile(targetPath.getParent(), "wopi_", ".tmp");
|
||||||
|
|
||||||
|
try (OutputStream os = Files.newOutputStream(tempFile, StandardOpenOption.WRITE)) {
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = content.read(buffer)) != -1) {
|
||||||
|
os.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.move(tempFile, targetPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
|
||||||
|
setFilePermissions(targetPath);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("文件保存失败: " + targetPath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path validateFilePath(String fileId) {
|
||||||
|
if (fileId.contains("..") || fileId.contains(":") || fileId.contains("\\\\")) {
|
||||||
|
throw new SecurityException("非法文件名: " + fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path basePath = Paths.get(BASE_DIR).normalize().toAbsolutePath();
|
||||||
|
Path resolvedPath = basePath.resolve(fileId).normalize();
|
||||||
|
|
||||||
|
if (!resolvedPath.startsWith(basePath)) {
|
||||||
|
throw new SecurityException("路径越界访问: " + fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFilePermissions(Path path) throws IOException {
|
||||||
|
if (!System.getProperty("os.name").toLowerCase().contains("win")) return;
|
||||||
|
|
||||||
|
String aclCommand = String.format(
|
||||||
|
"icacls \"%s\" /grant:r \"%s\":(F) /inheritance:r",
|
||||||
|
path.toString(),
|
||||||
|
System.getProperty("user.name")
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Runtime.getRuntime().exec(aclCommand).waitFor();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,3 @@
|
|||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: CollaboraOnline
|
name: CollaboraOnline
|
||||||
|
|
||||||
file:
|
|
||||||
preview:
|
|
||||||
collabora:
|
|
||||||
domain: http://192.168.3.211:9980
|
|
||||||
storageDomain: http://192.168.3.104:8080
|
|
||||||
|
Loading…
Reference in New Issue
Block a user