🎉
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>
|
||||
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
|
||||
</properties>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>jitpack.io</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@ -59,12 +53,6 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.github.wb04307201</groupId>-->
|
||||
<!-- <artifactId>file-preview-spring-boot-starter</artifactId>-->
|
||||
<!-- <version>1.2.10</version>-->
|
||||
<!-- </dependency>-->
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -1,8 +1,9 @@
|
||||
package vin.vio.collaboraonline;
|
||||
|
||||
import cn.wubo.file.preview.EnableFilePreview;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.filter.CommonsRequestLoggingFilter;
|
||||
|
||||
@SpringBootApplication
|
||||
public class CollaboraOnlineApplication {
|
||||
@ -10,5 +11,4 @@ public class CollaboraOnlineApplication {
|
||||
public static void main(String[] 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:
|
||||
application:
|
||||
name: CollaboraOnline
|
||||
|
||||
file:
|
||||
preview:
|
||||
collabora:
|
||||
domain: http://192.168.3.211:9980
|
||||
storageDomain: http://192.168.3.104:8080
|
||||
name: CollaboraOnline
|
Loading…
Reference in New Issue
Block a user