初步完成了WebDAV功能,同时还进行了一些细节改进。

This commit is contained in:
kohgylw@163.com 2022-06-02 10:09:05 +08:00
parent 86c53a075d
commit 642b42dc8c
133 changed files with 6322 additions and 354 deletions

View File

@ -1,7 +1,7 @@
## 欢迎访问kiftd源代码资源库
### Welcome to visit source of kiftd!
_当前版本v1.0.35-RELEASE_
_当前版本v1.0.36-SNAPSHOT_
### 简介
_kiftd——一款便捷、开源、功能完善的个人&团队&小型团队网盘服务器系统。_
@ -72,5 +72,5 @@ _提示源代码路径下包含了一些程序运行所需的非源代码资
### 联系作者?
如有任何需要(例如对该资源有疑问、意见或建议),请发件联系作者: kohgylw@163.com (青阳龙野),随时恭候您的来信!
青阳龙野@kohgylw by 2020年07月06
青阳龙野@kohgylw by 2022年06月02

View File

@ -179,3 +179,13 @@ test.auth.xxx=ucd
--------------
【已完成】修复了当用户执行批量文件上传操作时,如果中途切换浏览的文件夹可能会导致后续上传的目标文件夹不正确的问题。
【已完成】升级了内置的MySQL数据库驱动版本修复旧版本中存在的安全漏洞CVE-2019-2692
计划中 v1.0.36
--------------
【计划中】新增WebDAV支持功能该功能允许用户将kiftd挂载为一个“网络驱动器”并像访问本地文件夹那样访问kiftd中的文件。
开启方法在conf/server.properties中添加“webdav=enable”设置之后使用 http://{IP}:{端口}/dav/ 进行挂载。
【计划中】新增删除留档功能。启用该功能后,用户删除的文件将以原件的形式保留在指定路径内,从而使得管理者能够妥善处理这些被删除的文件。
【计划中】新增保留转码缓存的功能,启用该功能后,诸如视频转码缓存文件等临时文件将会在软件退出后继续保留,以便在服务器下次启动时重复使用而无需再次转码。
【已完成】优化了下载限速算法,现在下载限速的精度变得更高了。
【已完成】规范了下载功能中的ETag响应头的生成格式。
【已完成】将内置的JAVE升级为3.3.1版本。

View File

@ -4,7 +4,7 @@
<groupId>kohgylw</groupId>
<artifactId>kiftd</artifactId>
<version>1.0.35-RELEASE</version>
<version>1.0.36-SNAPSHOT</version>
<packaging>jar</packaging>
<name>kiftd</name>
@ -150,7 +150,7 @@
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-all-deps</artifactId>
<version>2.5.0</version>
<version>3.3.1</version>
</dependency>
<!-- end JAVE -->

View File

@ -9,9 +9,12 @@ import org.springframework.beans.factory.annotation.*;
import org.springframework.web.servlet.config.annotation.*;
import kohgylw.kiftd.server.util.*;
import kohgylw.kiftd.server.webdav.KiftdWebDAVServlet;
import java.io.*;
import javax.servlet.*;
import org.springframework.boot.web.servlet.*;
import org.springframework.context.annotation.*;
@ -26,24 +29,25 @@ import org.springframework.context.annotation.*;
* @version 1.0
*/
@Configurable
@ComponentScan({ "kohgylw.kiftd.server.controller", "kohgylw.kiftd.server.service.impl", "kohgylw.kiftd.server.util" })
@ComponentScan({ "kohgylw.kiftd.server.controller", "kohgylw.kiftd.server.service.impl", "kohgylw.kiftd.server.util",
"kohgylw.kiftd.server.webdav.util" })
@ServletComponentScan({ "kohgylw.kiftd.server.listener", "kohgylw.kiftd.server.filter" })
@Import({ DataAccess.class })
public class MVC extends ResourceHttpRequestHandler implements WebMvcConfigurer {
// 启用DefaultServlet用以处理可直接请求的静态资源
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
// 设置Web静态资源映射路径
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
// 将静态页面资源所在文件夹加入至资源路径中
registry.addResourceHandler(new String[] { "/**" }).addResourceLocations(new String[] {
"file:" + ConfigureReader.instance().getPath() + File.separator + "webContext" + File.separator });
}
// 生成上传管理器用于接收/缓存上传文件
@Bean
public MultipartConfigElement multipartConfigElement() {
@ -52,10 +56,16 @@ public class MVC extends ResourceHttpRequestHandler implements WebMvcConfigurer
factory.setLocation(ConfigureReader.instance().getTemporaryfilePath());
return factory.createMultipartConfig();
}
// 生成Gson实例用于服务Json序列化和反序列化
@Bean
public Gson gson() {
return new GsonBuilder().create();
}
// 注册WebDAV处理Servlet
@Bean
public ServletRegistrationBean<Servlet> WebDAVServlet() {
return new ServletRegistrationBean<Servlet>(new KiftdWebDAVServlet(), "/dav/*");
}
}

View File

@ -23,10 +23,11 @@ public class MastLoginFilter implements Filter {
final HttpServletResponse hsr = (HttpServletResponse) response;
final String url = hsq.getServletPath();
final HttpSession session = hsq.getSession();
if (url.startsWith("/externalLinksController/") || url.startsWith("//externalLinksController/")
if (url.startsWith("/externalLinksController") || url.startsWith("//externalLinksController")
|| url.startsWith("/homeController/getNewVerCode.do")
|| url.startsWith("//homeController/getNewVerCode.do")) {
chain.doFilter(request, response);// 对于外部链接控制器和验证码的请求直接放行
|| url.startsWith("//homeController/getNewVerCode.do") || url.startsWith("/dav")
|| url.startsWith("//dav")) {
chain.doFilter(request, response);// 对于外部链接控制器验证码和WebDAV的请求直接放行
return;
}
// 如果是无需登录的请求那么直接放行如果访问者已经登录那么会被后面的过滤器重定向至主页此处无需处理

View File

@ -9,11 +9,11 @@ import org.apache.commons.codec.digest.DigestUtils;
import kohgylw.kiftd.printer.Printer;
import kohgylw.kiftd.server.util.ConfigureReader;
import ws.schild.jave.Encoder;
import ws.schild.jave.EncoderProgressListener;
import ws.schild.jave.EncodingAttributes;
import ws.schild.jave.FFMPEGLocator;
import ws.schild.jave.MultimediaInfo;
import ws.schild.jave.MultimediaObject;
import ws.schild.jave.encode.EncodingAttributes;
import ws.schild.jave.info.MultimediaInfo;
import ws.schild.jave.process.ProcessLocator;
import ws.schild.jave.progress.EncoderProgressListener;
/**
*
@ -32,7 +32,7 @@ public class VideoTranscodeThread {
private Encoder encoder;
private String outputFileName;
public VideoTranscodeThread(File f, EncodingAttributes ea,FFMPEGLocator fl) throws Exception {
public VideoTranscodeThread(File f, EncodingAttributes ea,ProcessLocator fl) throws Exception {
// 首先计算MD5值
md5 = DigestUtils.md5Hex(new FileInputStream(f));
progress = "0.0";
@ -43,15 +43,15 @@ public class VideoTranscodeThread {
outputFileName="video_"+UUID.randomUUID().toString()+".mp4";
encoder.encode(mo, new File(ConfigureReader.instance().getTemporaryfilePath(), outputFileName),
ea, new EncoderProgressListener() {
public void sourceInfo(MultimediaInfo arg0) {
}
public void progress(int arg0) {
progress = (arg0 / 10.00) + "";
}
public void message(String arg0) {
}
public void sourceInfo(MultimediaInfo info) {
}
});
progress = "FIN";
} catch (Exception e) {

View File

@ -201,7 +201,7 @@ public class FileServiceImpl extends RangeFileStreamWriter implements FileServic
if (f.getFileName().equals(originalFileName)) {
try {
// 首先先将该节点中必须覆盖的信息更新
f.setFileSize(fbu.getFileSize(file));
f.setFileSize(fbu.getFileSize(file.getSize()));
f.setFileCreationDate(ServerTimeUtil.accurateToDay());
if (account != null) {
f.setFileCreator(account);
@ -267,7 +267,7 @@ public class FileServiceImpl extends RangeFileStreamWriter implements FileServic
if (block == null) {
return UPLOADERROR;
}
final String fsize = this.fbu.getFileSize(file);
final String fsize = this.fbu.getFileSize(file.getSize());
Node newNode = fbu.insertNewNode(fileName, account, block.getName(), fsize, folderId);
if (newNode != null) {
// 存入成功则写入日志并返回成功提示
@ -388,7 +388,8 @@ public class FileServiceImpl extends RangeFileStreamWriter implements FileServic
return "cannotRenameFile";
}
}
this.lu.writeRenameFileEvent(request, file, newFileName);
this.lu.writeRenameFileEvent(account, idg.getIpAddr(request), file.getFileParentFolder(), file.getFileName(),
newFileName);
return "renameFileSuccess";
}
@ -807,7 +808,7 @@ public class FileServiceImpl extends RangeFileStreamWriter implements FileServic
break;
}
} else {
// 不是直接删除冲突文件夹所有子文件夹
// 不是直接删除冲突文件夹及其所有子文件夹
fu.deleteAllChildFolder(f.getFolderId());
// 再将原文件夹移入目标文件夹内
folder.setFolderParent(locationpath);
@ -1087,8 +1088,8 @@ public class FileServiceImpl extends RangeFileStreamWriter implements FileServic
// 开始文件夹命名冲突检查若无重名则允许上传否则检查该文件夹是否具备覆盖条件具备该文件夹的访问权限且具备删除权限如无则可选择保留两者或取消
final List<Folder> folders = flm.queryByParentId(folderId);
try {
Folder testFolder = folders.stream().parallel()
.filter((n) -> n.getFolderName().equals(folderName)).findAny().get();
Folder testFolder = folders.stream().parallel().filter((n) -> n.getFolderName().equals(folderName))
.findAny().get();
if (ConfigureReader.instance().accessFolder(testFolder, account) && ConfigureReader.instance()
.authorized(account, AccountAuth.DELETE_FILE_OR_FOLDER, fu.getAllFoldersId(folderId))) {
cifr.setResult("repeatFolder_coverOrBoth");
@ -1204,7 +1205,7 @@ public class FileServiceImpl extends RangeFileStreamWriter implements FileServic
if (block == null) {
return UPLOADERROR;
}
final String fsize = this.fbu.getFileSize(file);
final String fsize = this.fbu.getFileSize(file.getSize());
Node newNode = fbu.insertNewNode(fileName, account, block.getName(), fsize, folderId);
if (newNode != null) {
// 成功则记录日志并返回成功提示
@ -1234,8 +1235,7 @@ public class FileServiceImpl extends RangeFileStreamWriter implements FileServic
* </p>
*
* @author 青阳龙野(kohgylw)
* @param path
* java.lang.String 原路径字符串
* @param path java.lang.String 原路径字符串
* @return java.lang.String[] 解析出的目录层级
*/
private String[] getParentPath(String path) {
@ -1260,8 +1260,7 @@ public class FileServiceImpl extends RangeFileStreamWriter implements FileServic
* </p>
*
* @author 青阳龙野(kohgylw)
* @param java.lang.String
* 需要解析的相对路径
* @param java.lang.String 需要解析的相对路径
* @return java.lang.String 文件名
*/
private String getFileNameFormPath(String path) {

View File

@ -30,6 +30,8 @@ public class FolderServiceImpl implements FolderService {
private LogUtil lu;
@Resource
private Gson gson;
@Resource
private IpAddrGetter idg;
public String newFolder(final HttpServletRequest request) {
final String parentId = request.getParameter("parentId");
@ -91,7 +93,7 @@ public class FolderServiceImpl implements FolderService {
final int r = this.fm.insertNewFolder(f);
if (r > 0) {
if (fu.isValidFolder(f)) {
this.lu.writeCreateFolderEvent(request, f);
this.lu.writeCreateFolderEvent(account, idg.getIpAddr(request), f);
return "createFolderSuccess";
} else {
return "cannotCreateFolder";
@ -193,7 +195,8 @@ public class FolderServiceImpl implements FolderService {
return "errorParameter";
}
}
this.lu.writeRenameFolderEvent(request, folder, newName, folderConstraint);
this.lu.writeRenameFolderEvent(account, idg.getIpAddr(request), folder.getFolderId(),
folder.getFolderName(), newName, folder.getFolderConstraint() + "", folderConstraint);
return "renameFolderSuccess";
}
} catch (Exception e) {
@ -313,7 +316,7 @@ public class FolderServiceImpl implements FolderService {
final int r = this.fm.insertNewFolder(f);
if (r > 0) {
if (fu.isValidFolder(f)) {
this.lu.writeCreateFolderEvent(request, f);
this.lu.writeCreateFolderEvent(account, idg.getIpAddr(request), f);
cnfbnr.setResult("success");
cnfbnr.setNewName(f.getFolderName());
return gson.toJson(cnfbnr);

View File

@ -50,7 +50,7 @@ public class PlayVideoServiceImpl implements PlayVideoService {
final String suffix = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
switch (suffix) {
case "mp4":
if (kfl.getFFMPEGExecutablePath() != null) {
if (kfl.getExecutablePath() != null) {
// 因此对于mp4后缀的视频进一步检查其编码是否为h264如果是则允许直接播放
File target = fbu.getFileFromBlocks(f);
if (target == null || !target.isFile()) {
@ -79,7 +79,7 @@ public class PlayVideoServiceImpl implements PlayVideoService {
case "avi":
case "wmv":
case "flv":
if (kfl.getFFMPEGExecutablePath() != null) {
if (kfl.getExecutablePath() != null) {
vi.setNeedEncode("Y");
} else {
vi.setNeedEncode("N");

View File

@ -100,7 +100,7 @@ public class ResourceServiceImpl implements ResourceService {
case ".wmv":
case ".mkv":
case ".flv":
if (kfl.getFFMPEGExecutablePath() != null) {
if (kfl.getExecutablePath() != null) {
contentType = "video/mp4";
synchronized (VideoTranscodeUtil.videoTranscodeThreads) {
VideoTranscodeThread vtt = VideoTranscodeUtil.videoTranscodeThreads.get(fid);
@ -369,7 +369,7 @@ public class ResourceServiceImpl implements ResourceService {
@Override
public String getVideoTranscodeStatus(HttpServletRequest request) {
if (kfl.getFFMPEGExecutablePath() != null) {
if (kfl.getExecutablePath() != null) {
String fId = request.getParameter("fileId");
if (fId != null) {
try {

View File

@ -85,6 +85,7 @@ public class ConfigureReader {
public static final int INVALID_IP_XFF_SETTING = 13;
public static final int INVALID_FFMPEG_SETTING = 14;
public static final int INVALID_MUST_LOGIN_SETTING = 15;
public static final int INVALID_WEBDAV_SETTING = 16;
public static final int LEGAL_PROPERTIES = 0;
private static Thread accountPropertiesUpdateDaemonThread;
private String timeZone;
@ -99,6 +100,7 @@ public class ConfigureReader {
private boolean ipXFFAnalysis = true;// 是否启用XFF解析
private boolean enableFFMPEG = true;// 是否启用视频播放的在线解码功能
private boolean enableDownloadByZip = true;// 是否启用打包下载功能
private boolean enableWebDAV = false;// 是否启用WebDAV功能
private static final int MAX_EXTENDSTORES_NUM = 255;// 扩展存储区最大数目
private static final String[] SYS_ACCOUNTS = { "SYS_IN", "Anonymous", "匿名用户" };// 一些系统的特殊账户
@ -184,13 +186,10 @@ public class ConfigureReader {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param account
* java.lang.String 账户的ID如果是匿名访问可传入null
* @param auth
* kohgylw.kiftd.server.enumeration.AccountAuth
* 要判断的操作类型使用枚举类中定义的各种操作作为参数传入
* @param folders
* 该操作所发生的文件夹序列其中应包含该操作对应的文件夹和其所有上级文件夹的ID
* @param account java.lang.String 账户的ID如果是匿名访问可传入null
* @param auth kohgylw.kiftd.server.enumeration.AccountAuth
* 要判断的操作类型使用枚举类中定义的各种操作作为参数传入
* @param folders 该操作所发生的文件夹序列其中应包含该操作对应的文件夹和其所有上级文件夹的ID
* @return boolean 是否具备该操作的权限若具备返回true否则返回false
*/
public boolean authorized(final String account, final AccountAuth auth, List<String> folders) {
@ -851,6 +850,23 @@ public class ConfigureReader {
} else {
enableDownloadByZip = true;
}
// 是否启用WebDAV功能
String webdavConf = serverp.getProperty("webdav");
if (webdavConf != null) {
switch (webdavConf) {
case "disable":
enableWebDAV = false;
break;
case "enable":
enableWebDAV = true;
break;
default:
Printer.instance.print("错误WebDAV功能的配置不正确只能设置为“disable”或“enable”请重新检查。");
return INVALID_WEBDAV_SETTING;
}
} else {
enableWebDAV = false;
}
Printer.instance.print("检查完毕。");
return LEGAL_PROPERTIES;
}
@ -976,10 +992,8 @@ public class ConfigureReader {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param f
* Folder 要访问的文件夹对象
* @param account
* String 要访问的账户
* @param f Folder 要访问的文件夹对象
* @param account String 要访问的账户
* @return boolean true允许访问false不允许访问
*/
public boolean accessFolder(Folder f, String account) {
@ -1108,8 +1122,7 @@ public class ConfigureReader {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param account
* java.lang.String 需要检查的账户名
* @param account java.lang.String 需要检查的账户名
* @return long 以byte为单位的最大阈值若返回0则设置错误若小于0则不限制
*/
public long getUploadFileSize(String account) {
@ -1142,8 +1155,7 @@ public class ConfigureReader {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param in
* java.lang.String 要转换的字符串内容格式应为{数值}{存储单位可选}例如1024KB10mb
* @param in java.lang.String 要转换的字符串内容格式应为{数值}{存储单位可选}例如1024KB10mb
* @return long 以Byte为单位计算的体积值若为0则代表设置错误若为负数则代表无限制
*/
private long getMaxSizeByString(String in) {
@ -1193,9 +1205,8 @@ public class ConfigureReader {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param account
* java.lang.String 需要获取限制的账户名
* @return long 最大下载速度限制以KB/s为单位
* @param account java.lang.String 需要获取限制的账户名
* @return long 最大下载速度限制以B/s为单位
*/
public long getDownloadMaxRate(String account) {
String defaultMaxRateP = accountp.getProperty("defaultMaxRate");
@ -1211,7 +1222,7 @@ public class ConfigureReader {
*
* <h2>下载限速设置值转化方法</h2>
* <p>
* 该方法用于将配置文件中的下载速度限制设置值转化为long类型的数值例如当输入字符串1输出1输入2MB输出2048
* 该方法用于将配置文件中的下载速度限制设置值转化为long类型的数值例如当输入字符串1输出1024输入2MB输出2097152
* </p>
* <p>
* 输入字符串格式规则{数值}{速率单位可选}其中速率单位可使用下列字符串之一指代不区分大小写
@ -1225,9 +1236,8 @@ public class ConfigureReader {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param in
* java.lang.String 要转换的字符串内容格式应为{数值}{速率单位可选}例如102410 mb
* @return long 以KB/s为单位计算的下载速度若为0则代表设置错误若为负数则代表无限制
* @param in java.lang.String 要转换的字符串内容格式应为{数值}{速率单位可选}例如102410 mb
* @return long 以B/s为单位计算的下载速度若为0则代表设置错误若为负数则代表无限制
*/
private long getMaxRateByString(String in) {
long r = 0L;
@ -1248,17 +1258,17 @@ public class ConfigureReader {
}
switch (unit) {
case "m":
r = Integer.parseInt(value) * 1024L;
break;
case "g":
r = Integer.parseInt(value) * 1048576L;
break;
case "g":
r = Integer.parseInt(value) * 1073741824L;
break;
default:
r = Integer.parseInt(in.trim());
r = Integer.parseInt(value) * 1024L;
break;
}
} else {
r = Integer.parseInt(in.trim());
r = Integer.parseInt(in.trim()) * 1024L;
}
} catch (Exception e) {
}
@ -1354,10 +1364,8 @@ public class ConfigureReader {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param account
* java.lang.String 账户ID该账户必须已经存在否则会导致修改失败
* @param newPassword
* java.lang.String 新密码必须不为null否则会导致修改失败
* @param account java.lang.String 账户ID该账户必须已经存在否则会导致修改失败
* @param newPassword java.lang.String 新密码必须不为null否则会导致修改失败
* @return boolean 操作是否成功成功则返回true
*/
public boolean changePassword(String account, String newPassword) throws Exception {
@ -1386,8 +1394,7 @@ public class ConfigureReader {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param ipAddr
* java.lang.String 要判断的IP地址
* @param ipAddr java.lang.String 要判断的IP地址
* @return boolean 该地址是否被禁用被禁用则返回true
*/
public boolean filterAccessIP(String ipAddr) {
@ -1451,13 +1458,10 @@ public class ConfigureReader {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param newAccount
* java.lang.String 要写入的新账户名
* @param newPassword
* java.lang.String 新账户的密码
* @param newAccount java.lang.String 要写入的新账户名
* @param newPassword java.lang.String 新账户的密码
* @return boolean 是否写入成功仅当账户名不存在且写入成功时返回true否则返回false
* @throws Exception
* 写入时发生的异常
* @throws Exception 写入时发生的异常
*/
public boolean createNewAccount(String newAccount, String newPassword) throws Exception {
if (newAccount != null && newPassword != null) {
@ -1538,4 +1542,18 @@ public class ConfigureReader {
public boolean isEnableDownloadByZip() {
return enableDownloadByZip;
}
/**
*
* <h2>判断用户是否启用了WebDAV功能</h2>
* <p>
* 判断用户是否启用了WebDAV功能
* </p>
*
* @author 青阳龙野(kohgylw)
* @return boolean 启用则返回true否则返回false
*/
public boolean isEnableWebDAV() {
return enableWebDAV;
}
}

View File

@ -11,6 +11,7 @@ import java.io.*;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import kohgylw.kiftd.server.model.*;
import kohgylw.kiftd.server.pojo.ExtendStores;
@ -79,13 +80,143 @@ public class FileBlockUtil {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param f
* MultipartFile 上传文件对象
* @param f MultipartFile 上传文件对象
* @return java.io.File 生成的文件块如果保存失败则返回null
*/
public File saveToFileBlocks(final MultipartFile f) {
// 如果存在扩展存储区则优先在已有文件块数目最少的扩展存储区中存放文件避免占用主文件系统
List<ExtendStores> ess = ConfigureReader.instance().getExtendStores();// 得到全部扩展存储区
// 得到全部扩展存储区
List<ExtendStores> ess = getExtendStoresBySort();
if (ess.size() > 0) {
// 从文件块最少的开始遍历这些扩展存储区
for (ExtendStores es : ess) {
if (es.getPath().getFreeSpace() > f.getSize()) {
// 如果该存储区的空余容量大于待上传文件的体积
File file = null;
try {
// 则尝试在该存储区中生成一个空文件块
file = createNewBlock(es.getIndex() + "_", es.getPath());
if (file != null) {
// 生成成功尝试存入数据
f.transferTo(file);
return file;
} else {
continue;// 如果本处无法生成新文件块那么在其他路径下继续尝试
}
} catch (IOException e) {
// 出现IO异常则删除残留文件并继续尝试其他扩展存储区
if (file != null) {
file.delete();
}
continue;
} catch (Exception e) {
// 出现其他异常则记录日志
lu.writeException(e);
Printer.instance.print(e.getMessage());
continue;
}
}
}
}
// 如果不存在扩展存储区或者最大的扩展存储区的剩余容量依旧小于指定大小则尝试在主文件系统路径下生成新文件块
File file = null;
try {
file = createNewBlock("file_", new File(ConfigureReader.instance().getFileBlockPath()));
if (file != null) {
// 生成成功则尝试存入数据
f.transferTo(file);
return file;
}
} catch (Exception e) {
// 出现异常则记录日志则删除残留数据并返回null
if (file != null) {
file.delete();
}
lu.writeException(e);
Printer.instance.print("错误:文件块生成失败,无法存入新的文件数据。详细信息:" + e.getMessage());
}
// 因其他原因生成失败也返回null
return null;
}
/**
*
* <h2>将新上传的文件存入文件系统</h2>
* <p>
* 将一个java.io.File类型的文件对象存入节点并返回保存的路径名称其中路径名称使用file_{UUID}.block
* 存放于主文件系统中{存储区编号}_{UUID}.block存放在指定编号的扩展存储区中的形式
* </p>
*
* @author 青阳龙野(kohgylw)
* @param f 要存入的文件对象
* @return java.io.File 生成的文件块如果保存失败则返回null
*/
public File saveToFileBlocks(final File f) {
// 得到全部扩展存储区
List<ExtendStores> ess = getExtendStoresBySort();
if (ess.size() > 0) {
// 从文件块最少的开始遍历这些扩展存储区
for (ExtendStores es : ess) {
if (es.getPath().getFreeSpace() > f.length()) {
// 如果该存储区的空余容量大于待上传文件的体积
File file = null;
try {
// 则尝试在该存储区中生成一个空文件块
file = createNewBlock(es.getIndex() + "_", es.getPath());
if (file != null) {
// 生成成功尝试存入数据
Files.move(f.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
return file;
} else {
continue;// 如果本处无法生成新文件块那么在其他路径下继续尝试
}
} catch (IOException e) {
// 出现IO异常则删除残留文件并继续尝试其他扩展存储区
if (file != null) {
file.delete();
}
continue;
} catch (Exception e) {
// 出现其他异常则记录日志
lu.writeException(e);
Printer.instance.print(e.getMessage());
continue;
}
}
}
}
// 如果不存在扩展存储区或者最大的扩展存储区的剩余容量依旧小于指定大小则尝试在主文件系统路径下生成新文件块
File file = null;
try {
file = createNewBlock("file_", new File(ConfigureReader.instance().getFileBlockPath()));
if (file != null) {
// 生成成功则尝试存入数据
Files.move(f.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
return file;
}
} catch (Exception e) {
// 出现异常则记录日志则删除残留数据并返回null
if (file != null) {
file.delete();
}
lu.writeException(e);
Printer.instance.print("错误:文件块生成失败,无法存入新的文件数据。详细信息:" + e.getMessage());
}
// 因其他原因生成失败也返回null
return null;
}
/**
*
* <h2>以剩余容量从小到大排序获取扩展存储区列表</h2>
* <p>
* 该方法用于获取所有扩展存储区的列表并按照剩余容量从小到大排序如果没有扩展存储区则返回一个长度为0的列表
* </p>
*
* @author 青阳龙野(kohgylw)
* @return java.util.List&lt;ExtendStores&rt; 排序好的扩展存储区列表
*/
private List<ExtendStores> getExtendStoresBySort() {
List<ExtendStores> ess = ConfigureReader.instance().getExtendStores();
if (ess.size() > 0) {
// 将所有扩展存储区按照已存储文件块的数目从小到大进行排序
Collections.sort(ess, new Comparator<ExtendStores>() {
@ -106,41 +237,8 @@ public class FileBlockUtil {
}
}
});
// 排序完毕后从文件块最少的开始遍历这些扩展存储区并尝试将新文件存入一个容量足够的扩展存储区中
for (ExtendStores es : ess) {
// 如果该存储区的空余容量大于要存放的文件
if (es.getPath().getFreeSpace() > f.getSize()) {
try {
File file = createNewBlock(es.getIndex() + "_", es.getPath());
if (file != null) {
f.transferTo(file);// 则执行存放并将文件命名为{存储区编号}_{UUID}.block的形式
return file;
} else {
continue;// 如果本处无法生成新的文件块那么在其他路径下继续尝试
}
} catch (IOException e) {
// 如果无法存入由于体积过大或其他问题那么继续尝试其他扩展存储区
continue;
} catch (Exception e) {
lu.writeException(e);
Printer.instance.print(e.getMessage());
continue;
}
}
}
}
// 如果不存在扩展存储区或者最大的扩展存储区无法存放目标文件则尝试将其存放至主文件系统路径下
try {
final File file = createNewBlock("file_", new File(ConfigureReader.instance().getFileBlockPath()));
if (file != null) {
f.transferTo(file);// 执行存放并肩文件命名为file_{UUID}.block的形式
return file;
}
} catch (Exception e) {
lu.writeException(e);
Printer.instance.print("错误:文件块生成失败,无法存入新的文件数据。详细信息:" + e.getMessage());
}
return null;
return ess;
}
// 生成创建一个在指定路径下名称编号绝对不重复的新文件块
@ -174,12 +272,10 @@ public class FileBlockUtil {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param f
* org.springframework.web.multipart.MultipartFile 上传文件对象
* @param size 文件的体积以Byte为单位
* @return java.lang.String 计算出来的体积以MB为单位
*/
public String getFileSize(final MultipartFile f) {
final long size = f.getSize();
public String getFileSize(final long size) {
final int mb = (int) (size / 1048576L);
return "" + mb;
}
@ -193,8 +289,7 @@ public class FileBlockUtil {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param f
* kohgylw.kiftd.server.model.Node 要删除的文件节点对象
* @param f kohgylw.kiftd.server.model.Node 要删除的文件节点对象
* @return boolean 删除结果true为成功
*/
public boolean deleteFromFileBlocks(Node f) {
@ -224,8 +319,7 @@ public class FileBlockUtil {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param f
* kohgylw.kiftd.server.model.Node 要获得的文件节点对象
* @param f kohgylw.kiftd.server.model.Node 要获得的文件节点对象
* @return java.io.File 对应的文件块抽象路径获取失败则返回null
*/
public File getFileFromBlocks(Node f) {
@ -314,12 +408,9 @@ public class FileBlockUtil {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param idList
* java.util.List<String> 要压缩的文件节点目标ID列表
* @param fidList
* java.util.List<String> 要压缩的文件夹目标ID列表迭代压缩
* @param account
* java.lang.String 用户ID用于判断压缩文件夹是否有效
* @param idList java.util.List<String> 要压缩的文件节点目标ID列表
* @param fidList java.util.List<String> 要压缩的文件夹目标ID列表迭代压缩
* @param account java.lang.String 用户ID用于判断压缩文件夹是否有效
* @return java.lang.String
* 压缩后产生的文件名称命名规则为tf_{UUID}.zip存放于文件系统中的temporaryfiles目录下
*/
@ -465,21 +556,20 @@ public class FileBlockUtil {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param block
* java.io.File 需要生成的文件块对象应为文件但也支持文件夹或者是null
* @param block java.io.File 需要生成的文件块对象应为文件但也支持文件夹或者是null
* @return java.lang.String 生成的ETag值当传入的block是null或其不存在时返回空字符串
*/
public String getETag(File block) {
if (block != null && block.exists()) {
StringBuffer sb = new StringBuffer();
sb.append("\"");
sb.append("W\"");
sb.append(block.length());
sb.append("-");
sb.append(block.lastModified());
sb.append("_");
sb.append(block.hashCode());
sb.append("\"");
return sb.toString().trim();
return sb.toString();
}
return "\"0\"";
return "W\"0-0\"";
}
/**
@ -490,16 +580,11 @@ public class FileBlockUtil {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param fileName
* java.lang.String 文件名称
* @param account
* java.lang.String 创建者账户若传入null则按匿名创建者处理
* @param filePath
* java.lang.String 文件节点对应的文件块索引
* @param fileSize
* java.lang.String 文件体积
* @param fileParentFolder
* java.lang.String 文件的父文件夹ID
* @param fileName java.lang.String 文件名称
* @param account java.lang.String 创建者账户若传入null则按匿名创建者处理
* @param filePath java.lang.String 文件节点对应的文件块索引
* @param fileSize java.lang.String 文件体积
* @param fileParentFolder java.lang.String 文件的父文件夹ID
* @return kohgylw.kiftd.server.model.Node 操作成功则返回节点对象否则返回null
*/
public Node insertNewNode(String fileName, String account, String filePath, String fileSize,
@ -547,8 +632,7 @@ public class FileBlockUtil {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param n
* kohgylw.kiftd.server.model.Node 待检查的节点
* @param n kohgylw.kiftd.server.model.Node 待检查的节点
* @return boolean 通过检查则返回true否则返回false并删除此节点
*/
public boolean isValidNode(Node n) {
@ -575,8 +659,7 @@ public class FileBlockUtil {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param n
* kohgylw.kiftd.server.model.Node 要获取路径的节点
* @param n kohgylw.kiftd.server.model.Node 要获取路径的节点
* @return java.lang.String 指定节点的逻辑路径包含其完整的上级文件夹路径和自身的文件名各级之间以/分割
*/
public String getNodePath(Node n) {

View File

@ -32,7 +32,7 @@ public class FileNodeUtil {
private static String url;// 当前链接的节点数据库位置
/**
* 文件夹内允许存放的最大文件和文件夹数目
* 文件夹内允许存放的最大文件和文件夹数目
*/
public static final int MAXIMUM_NUM_OF_SINGLE_FOLDER = Integer.MAX_VALUE;

View File

@ -12,19 +12,17 @@ import javax.annotation.Resource;
import org.springframework.stereotype.Component;
import kohgylw.kiftd.printer.Printer;
import ws.schild.jave.FFMPEGLocator;
import ws.schild.jave.Version;
import ws.schild.jave.process.ProcessLocator;
import ws.schild.jave.process.ProcessWrapper;
import ws.schild.jave.process.ffmpeg.FFMPEGProcess;
@Component
public class KiftdFFMPEGLocator extends FFMPEGLocator {
public class KiftdFFMPEGLocator implements ProcessLocator {
@Resource
private LogUtil lu;
/**
* 内置的ffmpeg引擎的版本号应该与jave整合资源本身自带的ffmpeg引擎版本对应
*/
private static final String MY_EXE_VERSION = "2.5.0";
private boolean enableFFmpeg;
private String suffix;
@ -64,10 +62,9 @@ public class KiftdFFMPEGLocator extends FFMPEGLocator {
}
@Override
public String getFFMPEGExecutablePath() {
public String getExecutablePath() {
// 每次获得路径时再次初始化ffmpeg执行路径
return initFFMPEGExecutablePath();// 这里的目的在于避免运行中ffmpeg被删掉从而导致程序找不到它
// 同时更新enableFFmpeg属性
return initFFMPEGExecutablePath();// 这里的目的在于避免运行中ffmpeg被删掉从而导致程序找不到它同时更新enableFFmpeg属性
}
// 初始化ffmpeg执行路径并返回过程包括
@ -100,7 +97,7 @@ public class KiftdFFMPEGLocator extends FFMPEGLocator {
} else {
// 否则使用内置的ffmpeg文件
// 临时文件中是否已经拷贝好了ffmpeg可执行文件了
ffmpegFile = new File(dirFolder, "ffmpeg-" + arch + "-" + MY_EXE_VERSION + suffix);
ffmpegFile = new File(dirFolder, "ffmpeg-" + arch + "-" + Version.getVersion() + suffix);
if (!ffmpegFile.exists()) {
// 没有那将自带的对应操作系统的ffmpeg文件拷贝到临时目录中如果没有对应自带的ffmpeg那么会抛出异常
// 如果抛出异常那么直接结束构造
@ -115,7 +112,6 @@ public class KiftdFFMPEGLocator extends FFMPEGLocator {
}
// 已经有了那么它应该准备好了
}
// 对于类Unix系统而言还要确保临时目录授予可运行权限以便jave运行时调用ffmpeg
if (!isWindows) {
if (!ffmpegFile.canExecute()) {
@ -129,7 +125,6 @@ public class KiftdFFMPEGLocator extends FFMPEGLocator {
}
}
}
// 上述工作都做好了就可以将ffmpeg的路径返回给jave调用了
// 如果到不了这里说明初始化失败该方法返回null那么应该禁用jave的在线转码功能
enableFFmpeg = true;
@ -138,13 +133,28 @@ public class KiftdFFMPEGLocator extends FFMPEGLocator {
// 把文件从自带的jar包中拷贝出来移入指定文件夹
private void copyFile(String path, File dest) {
String resourceName = "/ws/schild/jave/native/" + path;
String resourceName = "nativebin/" + path;
try {
if (!copy(getClass().getResourceAsStream(resourceName), dest.getAbsolutePath())) {
throw new NullPointerException();
InputStream is = getClass().getResourceAsStream(resourceName);
if (is == null) {
resourceName = "ws/schild/jave/nativebin/" + path;
is = ClassLoader.getSystemResourceAsStream(resourceName);
}
if (is == null) {
resourceName = "ws/schild/jave/nativebin/" + path;
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
is = classloader.getResourceAsStream(resourceName);
}
if (is != null) {
copy(is, dest.getAbsolutePath());
try {
is.close();
} catch (IOException ioex) {
lu.writeException(ioex);
}
}
} catch (NullPointerException ex) {
throw ex;
lu.writeException(ex);
}
}
@ -163,4 +173,8 @@ public class KiftdFFMPEGLocator extends FFMPEGLocator {
return enableFFmpeg;
}
@Override
public ProcessWrapper createExecutor() {
return new FFMPEGProcess(getExecutablePath());
}
}

View File

@ -66,8 +66,7 @@ public class LogUtil {
* 创建日志文件并写入异常信息当同日期的日志文件存在时则在其后面追加该信息
* </p>
*
* @param e
* Exception 需要记录的异常对象
* @param e Exception 需要记录的异常对象
*/
public void writeException(Exception e) {
if (ConfigureReader.instance().inspectLogLevel(LogLevel.Runtime_Exception)) {
@ -91,14 +90,12 @@ public class LogUtil {
* 写入新建文件夹信息包括操作者路劲及新文件夹名称
* </p>
*/
public void writeCreateFolderEvent(HttpServletRequest request, Folder f) {
public void writeCreateFolderEvent(String account, String ip, Folder f) {
if (ConfigureReader.instance().inspectLogLevel(LogLevel.Event)) {
String account = (String) request.getSession().getAttribute("ACCOUNT");
if (account == null || account.length() == 0) {
account = "Anonymous";
}
String a = account;// 方便下方使用终态操作
String ip = idg.getIpAddr(request);
writerThread.execute(() -> {
List<Folder> l = fu.getParentList(f.getFolderId());
String pl = new String();
@ -118,23 +115,22 @@ public class LogUtil {
* 写入重命名文件夹信息
* </p>
*/
public void writeRenameFolderEvent(HttpServletRequest request, Folder f, String newName, String newConstraint) {
public void writeRenameFolderEvent(String account, String ip, String folderId, String oldName, String newName,
String oldConstraint, String newConstraint) {
if (ConfigureReader.instance().inspectLogLevel(LogLevel.Event)) {
String account = (String) request.getSession().getAttribute("ACCOUNT");
if (account == null || account.length() == 0) {
account = "Anonymous";
}
String a = account;
String ip = idg.getIpAddr(request);
writerThread.execute(() -> {
List<Folder> l = fu.getParentList(f.getFolderId());
List<Folder> l = fu.getParentList(folderId);
String pl = new String();
for (Folder i : l) {
pl = pl + i.getFolderName() + "/";
}
String content = ">IP [" + ip + "]\r\n>ACCOUNT [" + a + "]\r\n>OPERATE [Edit folder]\r\n>PATH [" + pl
+ "]\r\n>NAME [" + f.getFolderName() + "]->[" + newName + "],CONSTRAINT ["
+ f.getFolderConstraint() + "]->[" + newConstraint + "]";
+ "]\r\n>NAME [" + oldName + "]->[" + newName + "],CONSTRAINT [" + oldConstraint + "]->["
+ newConstraint + "]";
writeToLog("Event", content);
});
}
@ -281,8 +277,7 @@ public class LogUtil {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param f
* kohgylw.kiftd.server.model.Node 下载目标
* @param f kohgylw.kiftd.server.model.Node 下载目标
*/
public void writeDownloadFileByKeyEvent(HttpServletRequest request, Node f) {
if (ConfigureReader.instance().inspectLogLevel(LogLevel.Event)) {
@ -339,23 +334,21 @@ public class LogUtil {
* 写入重命名文件信息
* </p>
*/
public void writeRenameFileEvent(HttpServletRequest request, Node f, String newName) {
public void writeRenameFileEvent(String account, String ip, String parentFolderId, String oldName, String newName) {
if (ConfigureReader.instance().inspectLogLevel(LogLevel.Event)) {
String account = (String) request.getSession().getAttribute("ACCOUNT");
if (account == null || account.length() == 0) {
account = "Anonymous";
}
String a = account;
String ip = idg.getIpAddr(request);
writerThread.execute(() -> {
Folder folder = fm.queryById(f.getFileParentFolder());
Folder folder = fm.queryById(parentFolderId);
List<Folder> l = fu.getParentList(folder.getFolderId());
String pl = new String();
for (Folder i : l) {
pl = pl + i.getFolderName() + "/";
}
String content = ">IP [" + ip + "]\r\n>ACCOUNT [" + a + "]\r\n>OPERATE [Rename file]\r\n>PATH [" + pl
+ folder.getFolderName() + "]\r\n>NAME [" + f.getFileName() + "]->[" + newName + "]";
+ folder.getFolderName() + "]\r\n>NAME [" + oldName + "]->[" + newName + "]";
writeToLog("Event", content);
});
}
@ -369,16 +362,11 @@ public class LogUtil {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param account
* java.lang.String 操作者账户名
* @param ip
* java.lang.String 操作者IP地址
* @param finalPath
* java.lang.String 被操作后的节点完整路径
* @param originPath
* java.lang.String 未操作前的节点完整路径
* @param isCopy
* boolean 是否为复制模式
* @param account java.lang.String 操作者账户名
* @param ip java.lang.String 操作者IP地址
* @param finalPath java.lang.String 被操作后的节点完整路径
* @param originPath java.lang.String 未操作前的节点完整路径
* @param isCopy boolean 是否为复制模式
*/
public void writeMoveFileEvent(String account, String ip, String originPath, String finalPath, boolean isCopy) {
if (ConfigureReader.instance().inspectLogLevel(LogLevel.Event)) {
@ -403,16 +391,11 @@ public class LogUtil {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param account
* java.lang.String 操作者账户名
* @param ip
* java.lang.String 操作者IP地址
* @param finalPath
* java.lang.String 被操作后的文件夹完整路径
* @param originPath
* java.lang.String 未操作前的文件夹完整路径
* @param isCopy
* boolean 是否为复制模式
* @param account java.lang.String 操作者账户名
* @param ip java.lang.String 操作者IP地址
* @param finalPath java.lang.String 被操作后的文件夹完整路径
* @param originPath java.lang.String 未操作前的文件夹完整路径
* @param isCopy boolean 是否为复制模式
*/
public void writeMoveFolderEvent(String account, String ip, String originPath, String finalPath, boolean isCopy) {
if (ConfigureReader.instance().inspectLogLevel(LogLevel.Event)) {
@ -428,7 +411,7 @@ public class LogUtil {
});
}
}
// 将文本信息以格式化标准写入日志文件中
private void writeToLog(String type, String content) {
String t = ServerTimeUtil.accurateToLogName();

View File

@ -12,8 +12,8 @@ import javax.servlet.http.HttpServletResponse;
*
* <h2>断点式文件输出流写出工具</h2>
* <p>
* 该工具负责处理断点下载请求并以相应规则写出文件流需要提供断点续传服务请继承该类并调用writeRangeFileStream方法
* 该操作已换回较为简单的RandomAccessFile实现效率与NIO相近更节省内存
* 该工具负责处理断点下载请求并以相应规则写出文件流需要提供断点续传服务请继承该类并调用writeRangeFileStream方法
* 若无法继承也可以直接静态调用此方法
* </p>
*
* @author 青阳龙野(kohgylw)
@ -31,26 +31,64 @@ public class RangeFileStreamWriter {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param request
* javax.servlet.http.HttpServletRequest 请求对象
* @param response
* javax.servlet.http.HttpServletResponse 响应对象
* @param fo
* java.io.File 需要写出的文件
* @param fname
* java.lang.String 文件名
* @param contentType
* java.lang.String HTTP Content-Type类型用于控制客户端行为
* @param maxRate
* long 最大输出速率以KB/s为单位若为负数则不限制输出速率用于限制客户端的下载速度
* @param eTag
* java.lang.String 资源的唯一性标识例如"aabbcc"
* @param isAttachment
* boolean 是否作为附件回传若希望用户下载则应设置为true
* @param request javax.servlet.http.HttpServletRequest 请求对象
* @param response javax.servlet.http.HttpServletResponse 响应对象
* @param fo java.io.File 需要写出的文件
* @param fname java.lang.String 文件名
* @param contentType java.lang.String HTTP Content-Type类型用于控制客户端行为
* @param maxRate long 最大输出速率以B/s为单位若为负数则不限制输出速率用于限制客户端的下载速度
* @param eTag java.lang.String 资源的唯一性标识例如"aabbcc"
* @param isAttachment boolean 是否作为附件回传若希望用户下载而非预览则应设置为true
* @return int 操作结束时返回的状态码
*/
protected int writeRangeFileStream(HttpServletRequest request, HttpServletResponse response, File fo, String fname,
String contentType, long maxRate, String eTag, boolean isAttachment) {
public static int writeRangeFileStream(HttpServletRequest request, HttpServletResponse response, File fo,
String fname, String contentType, long maxRate, String eTag, boolean isAttachment) {
return writeRangeFile(request, response, fo, fname, contentType, maxRate, eTag, isAttachment, true);
}
/**
*
* <h2>发送文件信息但仅返回响应头而不返回具体内容</h2>
* <p>
* 处理普通的或带有断点续传参数的下载请求并按照请求方式提供响应头信息不提供具体的文件内容
* </p>
*
* @author 青阳龙野(kohgylw)
* @param request javax.servlet.http.HttpServletRequest 请求对象
* @param response javax.servlet.http.HttpServletResponse 响应对象
* @param fo java.io.File 需要写出的文件
* @param fname java.lang.String 文件名
* @param contentType java.lang.String HTTP Content-Type类型用于控制客户端行为
* @param eTag java.lang.String 资源的唯一性标识例如"aabbcc"
* @param isAttachment boolean 是否作为附件回传若希望用户下载而非预览则应设置为true
* @return int 操作结束时返回的状态码
*/
public static int writeRangeFileHead(HttpServletRequest request, HttpServletResponse response, File fo,
String fname, String contentType, String eTag, boolean isAttachment) {
return writeRangeFile(request, response, fo, fname, contentType, -1, eTag, isAttachment, false);
}
/**
*
* <h2>回传文件数据可选择是否发送具体的文件内容</h2>
* <p>
* 该方法用于提供对文件下载请求的处理并按照请求方式提供相应的输出流写出当选择发送具体的文件内容时将会正常返回文件内容 否则仅返回响应头而无响应体
* </p>
*
* @author 青阳龙野(kohgylw)
* @param request javax.servlet.http.HttpServletRequest 请求对象
* @param response javax.servlet.http.HttpServletResponse 响应对象
* @param fo java.io.File 需要写出的文件
* @param fname java.lang.String 文件名
* @param contentType java.lang.String HTTP Content-Type类型用于控制客户端行为
* @param maxRate long 最大输出速率以B/s为单位若为负数则不限制输出速率用于限制客户端的下载速度
* @param eTag java.lang.String 资源的唯一性标识例如"aabbcc"
* @param isAttachment boolean 是否作为附件回传若希望用户下载则应设置为true
* @param sendBody boolean 是否发送具体的文件内容若设置为false则仅返回响应头
* @return int 操作结束时返回的状态码
*/
private static int writeRangeFile(HttpServletRequest request, HttpServletResponse response, File fo, String fname,
String contentType, long maxRate, String eTag, boolean isAttachment, boolean sendBody) {
long fileLength = fo.length();// 文件总大小
long startOffset = 0; // 起始偏移量
boolean hasEnd = false;// 请求区间是否存在结束标识
@ -144,45 +182,50 @@ public class RangeFileStreamWriter {
} else { // 从开始进行下载
contentLength = fileLength; // 客户端要求全文下载
}
response.setHeader("Content-Length", "" + contentLength);// 设置请求体长度
// 写出缓冲
byte[] buf = new byte[ConfigureReader.instance().getBuffSize()];
// 读取文件并写处至输出流
try (RandomAccessFile raf = new RandomAccessFile(fo, "r")) {
BufferedOutputStream out = maxRate >= 0
? new VariableSpeedBufferedOutputStream(response.getOutputStream(), maxRate, request.getSession())
: new BufferedOutputStream(response.getOutputStream());
raf.seek(startOffset);
if (!hasEnd) {
// 无结束偏移量时将其从起始偏移量开始写到文件整体结束如果从头开始下载起始偏移量为0
int n = 0;
while ((n = raf.read(buf)) != -1) {
out.write(buf, 0, n);
}
} else {
// 有结束偏移量时将其从起始偏移量开始写至指定偏移量结束
int n = 0;
long readLength = 0;// 写出量用于确定结束位置
while (readLength < contentLength) {
n = raf.read(buf);
readLength += n;
out.write(buf, 0, n);
}
}
out.flush();
out.close();
return status;
} catch (IOException ex) {
// 针对任何IO异常忽略传输失败不处理
status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
return status;
} catch (IllegalArgumentException e) {
status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
try {
response.sendError(status);
} catch (IOException e1) {
}
return status;
if (sendBody) {
response.setHeader("Content-Length", "" + contentLength);// 设置请求体长度
} else {
response.setHeader("Content-Length", "0");
}
if (sendBody) {
// 写出缓冲
byte[] buf = new byte[ConfigureReader.instance().getBuffSize()];
// 读取文件并写处至输出流
try (RandomAccessFile raf = new RandomAccessFile(fo, "r")) {
BufferedOutputStream out = maxRate >= 0
? new VariableSpeedBufferedOutputStream(response.getOutputStream(), maxRate,
request.getSession())
: new BufferedOutputStream(response.getOutputStream());
raf.seek(startOffset);
if (!hasEnd) {
// 无结束偏移量时将其从起始偏移量开始写到文件整体结束如果从头开始下载起始偏移量为0
int n = 0;
while ((n = raf.read(buf)) != -1) {
out.write(buf, 0, n);
}
} else {
// 有结束偏移量时将其从起始偏移量开始写至指定偏移量结束
int n = 0;
long readLength = 0;// 写出量用于确定结束位置
while (readLength < contentLength) {
n = raf.read(buf);
readLength += n;
out.write(buf, 0, (int) (readLength <= contentLength ? n : n - (readLength - contentLength)));
}
}
out.flush();
out.close();
} catch (IOException ex) {
// 针对任何IO异常忽略传输失败不处理
status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
} catch (IllegalArgumentException e) {
status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
try {
response.sendError(status);
} catch (IOException e1) {
}
}
}
return status;
}
}

View File

@ -3,10 +3,14 @@ package kohgylw.kiftd.server.util;
import java.util.*;
import java.io.File;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalQueries;
/**
*
@ -55,8 +59,7 @@ public class ServerTimeUtil {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param block
* java.io.File 要生成的文件块对象应该是文件但也支持文件夹或者null
* @param block java.io.File 要生成的文件块对象应该是文件但也支持文件夹或者null
* @return java.lang.String 记录最后修改日期的时间截格式类似于Wed, 29 Apr 2020 08:18:43
* GMT若传入文件不存在或为null则返回当前时间
*/
@ -69,6 +72,29 @@ public class ServerTimeUtil {
}
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z", Locale.US)
.withZone(ZoneId.of("GMT"));
return longToTime.format(dtf).trim();
return longToTime.format(dtf);
}
/**
*
* <h2>将日期字符串精确到日转化为时间值</h2>
* <p>
* 该方法用于将本工具生成的日期字符串精确到日转化成以毫秒为单位的long型时间
* 该时间从1970-01-01T00:00:00Z开始计数
* </p>
*
* @author 青阳龙野(kohgylw)
* @param date 要转化的日期字符串精确到日该字符串应由本类中的String accurateToDay()方法生成
* @return long 时间以毫秒为单位从1970-01-01T00:00:00Z开始计数如果无法转化例如传入--则返回0
*/
public static long getTimeFromDateAccurateToDay(String date) {
try {
DateTimeFormatter dtfDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
LocalDate ld = dtfDateTimeFormatter.parse(date, TemporalQueries.localDate());
Instant instant = ld.atTime(LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault()).toInstant();
return instant.toEpochMilli();
} catch (DateTimeParseException e) {
return 0L;
}
}
}

View File

@ -22,8 +22,10 @@ import javax.servlet.http.HttpSession;
*/
public class VariableSpeedBufferedOutputStream extends BufferedOutputStream {
private long maxRate;// 该实例的最大输出限速KB/s为单位
private long maxRate;// 该实例的最大输出限速B/s为单位
private HttpSession session;// 该实例所用的用户会话对象用于线程锁
private long writtenLength;// 一秒之内已经写出的数据长度以B为单位
private long startTime;// 计时起始时间以毫秒为单位
/**
*
@ -33,18 +35,17 @@ public class VariableSpeedBufferedOutputStream extends BufferedOutputStream {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param out
* java.io.OutputStream 原始输出流传入方法与普通的BufferedOutputStream构造器相同
* @param maxRate
* long 每秒最大可输出的数据数量以KB为单位例如传入100就代表该实例的最大输出限速为100 KB/s
* @param session
* javax.servlet.http.HttpSession 传入用户会话该对象用于锁定输出操作从而确保当用户开启多个
* 下载任务时总的最大下载速率仍不会超过限速值
* @param out java.io.OutputStream 原始输出流传入方法与普通的BufferedOutputStream构造器相同
* @param maxRate long 每秒最大可输出的数据数量以B为单位例如传入1024就代表该实例的最大输出限速为1KB/s
* @param session javax.servlet.http.HttpSession 传入用户会话该对象用于锁定输出操作从而确保当用户开启多个
* 下载任务时总的最大下载速率仍不会超过限速值
*/
public VariableSpeedBufferedOutputStream(OutputStream out, long maxRate, HttpSession session) {
super(out);
this.maxRate = maxRate;
this.session = session;
this.writtenLength = 0;
this.startTime = System.currentTimeMillis();
}
/**
@ -56,37 +57,51 @@ public class VariableSpeedBufferedOutputStream extends BufferedOutputStream {
* </p>
*
* @author 青阳龙野(kohgylw)
* @param b
* byte[] 数据数组
* @param off
* int 数据的起始下标
* @param len
* int 数据在数组中的长度
* @param b byte[] 数据数组
* @param off int 数据的起始下标
* @param len int 数据在数组中的长度
*/
public void write(byte[] b, int off, int len) throws IOException {
if (maxRate > 0) {
// 如果限速值为正数那么进行限速输出输出时要求独占Session对象以确保和其它下载任务共享最大限速
synchronized (session) {
// 记录写出前的纳秒值
long startNano = System.nanoTime();
super.write(b, off, len);// 执行写出
// 记录写出后的纳秒值
long endNano = System.nanoTime();
int n = len - off;// 计算本次写出的数据长度byte数
if (n > 0) {// 确实写出了数据
long consumeNano = endNano - startNano;// 记录写出的耗时纳秒
long shouldNano = 976562L * (long)((double) n / maxRate);// 计算最大应耗时纳秒
if (consumeNano < shouldNano) {// 如果实际耗时小于应耗时则代表写出过快应进行延迟
// 计算还需要再延迟的毫秒数
long shouldSleep = (shouldNano - consumeNano) / 1000000;
if (shouldSleep > 0) {// 延迟毫秒数大于0
int startIndex = off;// 记录当前应读数组的起始位置
int surplusLength = len;// 记录数组中应写的剩余数据量
while (surplusLength > 0) {
synchronized (session) {
// 如果数组内仍有数据存留则执行写出操作
// 如果尚未开始计量一秒内的写出量则记录写出前的毫秒值
if (writtenLength == 0) {
startTime = System.currentTimeMillis();
}
// 计算此秒之内最多还能写出多少数据
long shouldWriteLength = maxRate - writtenLength;
if (surplusLength > shouldWriteLength) {
// 如果数组内数据的剩余量大于此秒允许写出量则写出允许写出量的数据
super.write(b, startIndex, (int) shouldWriteLength);
startIndex += shouldWriteLength;
writtenLength += shouldWriteLength;
surplusLength -= shouldWriteLength;
} else {
// 如果数组内数据的剩余量小于等于此秒的应写量则将剩余量的数据全部写出
super.write(b, startIndex, surplusLength);
startIndex += surplusLength;
writtenLength += surplusLength;
surplusLength -= surplusLength;
}
if (writtenLength >= maxRate) {
// 如果已写出量达到了每秒允许的最大写出量则计算实际耗时
long consumeTime = System.currentTimeMillis() - startTime;
if (consumeTime < 1000) {
// 如果实际耗时小于1秒那么睡够一秒
try {
Thread.sleep(shouldSleep);// 执行延迟
Thread.sleep(1000 - consumeTime);
} catch (InterruptedException e) {
// 如果收到中断指令那么就响应中断
Thread.currentThread().interrupt();
}
}
// 写出量计数归0以便下次写出时重新计量
writtenLength = 0;
}
}
}
@ -97,5 +112,4 @@ public class VariableSpeedBufferedOutputStream extends BufferedOutputStream {
throw new IllegalArgumentException("Error:invalid maximum download rate value.");
}
}
}

View File

@ -13,9 +13,9 @@ import org.springframework.stereotype.Component;
import kohgylw.kiftd.server.mapper.NodeMapper;
import kohgylw.kiftd.server.model.Node;
import kohgylw.kiftd.server.pojo.VideoTranscodeThread;
import ws.schild.jave.AudioAttributes;
import ws.schild.jave.EncodingAttributes;
import ws.schild.jave.VideoAttributes;
import ws.schild.jave.encode.AudioAttributes;
import ws.schild.jave.encode.EncodingAttributes;
import ws.schild.jave.encode.VideoAttributes;
/**
*
@ -49,7 +49,7 @@ public class VideoTranscodeUtil {
VideoAttributes video = new VideoAttributes();
video.setCodec("libx264");
ea = new EncodingAttributes();
ea.setFormat("MP4");
ea.setOutputFormat("MP4");
ea.setVideoAttributes(video);
ea.setAudioAttributes(audio);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,73 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kohgylw.kiftd.server.webdav.date;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Queue;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* A thread safe wrapper around {@link SimpleDateFormat} that does not make use
* of ThreadLocal and - broadly - only creates enough SimpleDateFormat objects
* to satisfy the concurrency requirements.
*/
public class ConcurrentDateFormat {
private final String format;
private final Locale locale;
private final TimeZone timezone;
private final Queue<SimpleDateFormat> queue = new ConcurrentLinkedQueue<>();
public ConcurrentDateFormat(String format, Locale locale, TimeZone timezone) {
this.format = format;
this.locale = locale;
this.timezone = timezone;
SimpleDateFormat initial = createInstance();
queue.add(initial);
}
public String format(Date date) {
SimpleDateFormat sdf = queue.poll();
if (sdf == null) {
sdf = createInstance();
}
String result = sdf.format(date);
queue.add(sdf);
return result;
}
public Date parse(String source) throws ParseException {
SimpleDateFormat sdf = queue.poll();
if (sdf == null) {
sdf = createInstance();
}
Date result = sdf.parse(source);
sdf.setTimeZone(timezone);
queue.add(sdf);
return result;
}
private SimpleDateFormat createInstance() {
SimpleDateFormat sdf = new SimpleDateFormat(format, locale);
sdf.setTimeZone(timezone);
return sdf;
}
}

View File

@ -0,0 +1,219 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kohgylw.kiftd.server.webdav.date;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
/**
* Utility class to generate HTTP dates.
*
* @author Remy Maucherat
*/
public final class FastHttpDateFormat {
// -------------------------------------------------------------- Variables
private static final int CACHE_SIZE =
Integer.parseInt(System.getProperty("org.apache.tomcat.util.http.FastHttpDateFormat.CACHE_SIZE", "1000"));
/**
* The only date format permitted when generating HTTP headers.
*
* @deprecated Unused. This will be removed in Tomcat 10.
*/
@Deprecated
public static final String RFC1123_DATE = "EEE, dd MMM yyyy HH:mm:ss zzz";
// HTTP date formats
private static final String DATE_RFC5322 = "EEE, dd MMM yyyy HH:mm:ss z";
private static final String DATE_OBSOLETE_RFC850 = "EEEEEE, dd-MMM-yy HH:mm:ss zzz";
private static final String DATE_OBSOLETE_ASCTIME = "EEE MMMM d HH:mm:ss yyyy";
private static final ConcurrentDateFormat FORMAT_RFC5322;
private static final ConcurrentDateFormat FORMAT_OBSOLETE_RFC850;
private static final ConcurrentDateFormat FORMAT_OBSOLETE_ASCTIME;
private static final ConcurrentDateFormat[] httpParseFormats;
static {
// All the formats that use a timezone use GMT
TimeZone tz = TimeZone.getTimeZone("GMT");
FORMAT_RFC5322 = new ConcurrentDateFormat(DATE_RFC5322, Locale.US, tz);
FORMAT_OBSOLETE_RFC850 = new ConcurrentDateFormat(DATE_OBSOLETE_RFC850, Locale.US, tz);
FORMAT_OBSOLETE_ASCTIME = new ConcurrentDateFormat(DATE_OBSOLETE_ASCTIME, Locale.US, tz);
httpParseFormats = new ConcurrentDateFormat[] {
FORMAT_RFC5322, FORMAT_OBSOLETE_RFC850, FORMAT_OBSOLETE_ASCTIME };
}
/**
* Instant on which the currentDate object was generated.
*/
private static volatile long currentDateGenerated = 0L;
/**
* Current formatted date.
*/
private static String currentDate = null;
/**
* Formatter cache.
*/
private static final Map<Long, String> formatCache = new ConcurrentHashMap<>(CACHE_SIZE);
/**
* Parser cache.
*/
private static final Map<String, Long> parseCache = new ConcurrentHashMap<>(CACHE_SIZE);
// --------------------------------------------------------- Public Methods
/**
* Get the current date in HTTP format.
* @return the HTTP date
*/
public static final String getCurrentDate() {
long now = System.currentTimeMillis();
// Handle case where time moves backwards (e.g. system time corrected)
if (Math.abs(now - currentDateGenerated) > 1000) {
currentDate = FORMAT_RFC5322.format(new Date(now));
currentDateGenerated = now;
}
return currentDate;
}
/**
* Get the HTTP format of the specified date.
* @param value The date
* @param threadLocalformat Ignored. The local ConcurrentDateFormat will
* always be used.
* @return the HTTP date
*
* @deprecated Unused. This will be removed in Tomcat 10
*/
@Deprecated
public static final String formatDate(long value, DateFormat threadLocalformat) {
return formatDate(value);
}
/**
* Get the HTTP format of the specified date.
* @param value The date
* @return the HTTP date
*/
public static final String formatDate(long value) {
Long longValue = Long.valueOf(value);
String cachedDate = formatCache.get(longValue);
if (cachedDate != null) {
return cachedDate;
}
String newDate = FORMAT_RFC5322.format(new Date(value));
updateFormatCache(longValue, newDate);
return newDate;
}
/**
* Try to parse the given date as an HTTP date.
* @param value The HTTP date
* @param threadLocalformats Ignored. The local array of
* ConcurrentDateFormat will always be used.
* @return the date as a long
*
* @deprecated Unused. This will be removed in Tomcat 10
* Use {@link #parseDate(String)}
*/
@Deprecated
public static final long parseDate(String value, DateFormat[] threadLocalformats) {
return parseDate(value);
}
/**
* Try to parse the given date as an HTTP date.
* @param value The HTTP date
* @return the date as a long or <code>-1</code> if the value cannot be
* parsed
*/
public static final long parseDate(String value) {
Long cachedDate = parseCache.get(value);
if (cachedDate != null) {
return cachedDate.longValue();
}
long date = -1;
for (int i = 0; (date == -1) && (i < httpParseFormats.length); i++) {
try {
date = httpParseFormats[i].parse(value).getTime();
updateParseCache(value, Long.valueOf(date));
} catch (ParseException e) {
// Ignore
}
}
return date;
}
/**
* Update cache.
*/
private static void updateFormatCache(Long key, String value) {
if (value == null) {
return;
}
if (formatCache.size() > CACHE_SIZE) {
formatCache.clear();
}
formatCache.put(key, value);
}
/**
* Update cache.
*/
private static void updateParseCache(String key, Long value) {
if (value == null) {
return;
}
if (parseCache.size() > CACHE_SIZE) {
parseCache.clear();
}
parseCache.put(key, value);
}
}

View File

@ -0,0 +1,164 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kohgylw.kiftd.server.webdav.dom;
import java.io.PrintWriter;
import java.io.Writer;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* A DOM writer optimised for use by WebDAV.
*/
public class DOMWriter {
private final PrintWriter out;
public DOMWriter(Writer writer) {
out = new PrintWriter(writer);
}
/**
* Prints the specified node, recursively.
* @param node The node to output
*/
public void print(Node node) {
// is there anything to do?
if (node == null) {
return;
}
int type = node.getNodeType();
switch (type) {
// print document
case Node.DOCUMENT_NODE:
print(((Document) node).getDocumentElement());
out.flush();
break;
// print element with attributes
case Node.ELEMENT_NODE:
out.print('<');
out.print(node.getLocalName());
Attr attrs[] = sortAttributes(node.getAttributes());
for (Attr attr : attrs) {
out.print(' ');
out.print(attr.getLocalName());
out.print("=\"");
out.print(Escape.xml("", true, attr.getNodeValue()));
out.print('"');
}
out.print('>');
printChildren(node);
break;
// handle entity reference nodes
case Node.ENTITY_REFERENCE_NODE:
printChildren(node);
break;
// print cdata sections
case Node.CDATA_SECTION_NODE:
out.print(Escape.xml("", true, node.getNodeValue()));
break;
// print text
case Node.TEXT_NODE:
out.print(Escape.xml("", true, node.getNodeValue()));
break;
// print processing instruction
case Node.PROCESSING_INSTRUCTION_NODE:
out.print("<?");
out.print(node.getLocalName());
String data = node.getNodeValue();
if (data != null && data.length() > 0) {
out.print(' ');
out.print(data);
}
out.print("?>");
break;
}
if (type == Node.ELEMENT_NODE) {
out.print("</");
out.print(node.getLocalName());
out.print('>');
}
out.flush();
} // print(Node)
private void printChildren(Node node) {
NodeList children = node.getChildNodes();
if (children != null) {
int len = children.getLength();
for (int i = 0; i < len; i++) {
print(children.item(i));
}
}
}
/**
* Returns a sorted list of attributes.
* @param attrs The map to sort
* @return a sorted attribute array
*/
private Attr[] sortAttributes(NamedNodeMap attrs) {
if (attrs == null) {
return new Attr[0];
}
int len = attrs.getLength();
Attr array[] = new Attr[len];
for (int i = 0; i < len; i++) {
array[i] = (Attr) attrs.item(i);
}
for (int i = 0; i < len - 1; i++) {
String name = null;
name = array[i].getLocalName();
int index = i;
for (int j = i + 1; j < len; j++) {
String curName = null;
curName = array[j].getLocalName();
if (curName.compareTo(name) < 0) {
name = curName;
index = j;
}
}
if (index != i) {
Attr temp = array[i];
array[i] = array[index];
array[index] = temp;
}
}
return array;
}
}

View File

@ -0,0 +1,165 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kohgylw.kiftd.server.webdav.dom;
/**
* Provides utility methods to escape content for different contexts. It is
* critical that the escaping used is correct for the context in which the data
* is to be used.
*/
public class Escape {
private Escape() {
// Hide default constructor for this utility class
}
/**
* Escape content for use in HTML. This escaping is suitable for the
* following uses:
* <ul>
* <li>Element content when the escaped data will be placed directly inside
* tags such as &lt;p&gt;, &lt;td&gt; etc.</li>
* <li>Attribute values when the attribute value is quoted with &quot; or
* &#x27;.</li>
* </ul>
*
* @param content The content to escape
*
* @return The escaped content or {@code null} if the content was
* {@code null}
*/
public static String htmlElementContent(String content) {
if (content == null) {
return null;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < content.length(); i++) {
char c = content.charAt(i);
if (c == '<') {
sb.append("&lt;");
} else if (c == '>') {
sb.append("&gt;");
} else if (c == '\'') {
sb.append("&#39;");
} else if (c == '&') {
sb.append("&amp;");
} else if (c == '"') {
sb.append("&quot;");
} else if (c == '/') {
sb.append("&#47;");
} else {
sb.append(c);
}
}
return (sb.length() > content.length()) ? sb.toString() : content;
}
/**
* Convert the object to a string via {@link Object#toString()} and HTML
* escape the resulting string for use in HTML content.
*
* @param obj The object to convert to String and then escape
*
* @return The escaped content or <code>&quot;?&quot;</code> if obj is
* {@code null}
*/
public static String htmlElementContent(Object obj) {
if (obj == null) {
return "?";
}
try {
return htmlElementContent(obj.toString());
} catch (Exception e) {
return null;
}
}
/**
* Escape content for use in XML.
*
* @param content The content to escape
*
* @return The escaped content or {@code null} if the content was
* {@code null}
*/
public static String xml(String content) {
return xml(null, content);
}
/**
* Escape content for use in XML.
*
* @param ifNull The value to return if content is {@code null}
* @param content The content to escape
*
* @return The escaped content or the value of {@code ifNull} if the
* content was {@code null}
*/
public static String xml(String ifNull, String content) {
return xml(ifNull, false, content);
}
/**
* Escape content for use in XML.
*
* @param ifNull The value to return if content is {@code null}
* @param escapeCRLF Should CR and LF also be escaped?
* @param content The content to escape
*
* @return The escaped content or the value of ifNull if the content was
* {@code null}
*/
public static String xml(String ifNull, boolean escapeCRLF, String content) {
if (content == null) {
return ifNull;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < content.length(); i++) {
char c = content.charAt(i);
if (c == '<') {
sb.append("&lt;");
} else if (c == '>') {
sb.append("&gt;");
} else if (c == '\'') {
sb.append("&apos;");
} else if (c == '&') {
sb.append("&amp;");
} else if (c == '"') {
sb.append("&quot;");
} else if (escapeCRLF && c == '\r') {
sb.append("&#13;");
} else if (escapeCRLF && c == '\n') {
sb.append("&#10;");
} else {
sb.append(c);
}
}
return (sb.length() > content.length()) ? sb.toString(): content;
}
}

View File

@ -0,0 +1,14 @@
package kohgylw.kiftd.server.webdav.exception;
/**
*
* <h2>未授权异常</h2>
* <p>该异常用于表名在WebDAV操作中未进行合适的授权</p>
* @author 青阳龙野(kohgylw)
* @version 1.0
*/
public class UnAuthorizedException extends Exception {
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,327 @@
package kohgylw.kiftd.server.webdav.pojo;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.cert.Certificate;
import java.util.jar.Manifest;
import org.apache.catalina.WebResource;
import org.apache.catalina.WebResourceRoot;
import kohgylw.kiftd.server.model.Folder;
import kohgylw.kiftd.server.model.Node;
import kohgylw.kiftd.server.util.ServerTimeUtil;
import kohgylw.kiftd.server.webdav.date.FastHttpDateFormat;
import kohgylw.kiftd.server.webdav.url.HttpPathUtil;
/**
*
* <h2>封装WebDAV请求的文件或文件夹的资源类</h2>
* <p>
* 该类用于将kiftd中的文件或文件夹封装为一个抽象的资源以便进行统一处理避免文件和文件夹区分处理带来的代码混乱
* </p>
*
* @author 青阳龙野(kohgylw)
* @version 1.0
*/
public class KiftdWebDAVResource implements WebResource {
private String path;// 该资源的路径
private boolean isExists;// 该资源是否存在
private boolean isDirectory;// 该资源是一个文件夹
private long creation;// 创建日期
private String name;// 资源名称
private String mimeType;// Mime Type
private volatile String weakETag;// 存储ETag声明
private Node node;// 如果资源是个文件那么该属性对应文件节点对象
private File fileBlock;// 如果资源是个文件那么该属性对应文件块对象
private Folder folder;// 如果资源是个文件夹那么该属性对应文件夹对象
/**
*
* <h2>构造一个文件夹资源</h2>
* <p>
* 该方法将构造一个文件夹对应的资源
* </p>
*
* @author 青阳龙野(kohgylw)
* @param path 资源路径必须输入完整的路径名称/开头例如/foo/foo/bar
* @param folder 文件夹对象如果文件夹不存在则可传入null
*/
public KiftdWebDAVResource(String path, Folder folder) {
this.path = path;
this.folder = folder;
if (this.path != null && this.folder != null) {
if (!path.endsWith("/")) {
// 规范化路径名文件夹路径统一以/结尾以符合URL标准
this.path = path + "/";
}
isExists = true;
isDirectory = true;
name = folder.getFolderName();
creation = ServerTimeUtil.getTimeFromDateAccurateToDay(this.folder.getFolderCreationDate());
} else {
this.name = HttpPathUtil.getResourceName(this.path);
}
}
/**
*
* <h2>构造一个文件资源</h2>
* <p>
* 该方法将构造一个文件对应的资源
* </p>
*
* @author 青阳龙野(kohgylw)
* @param path 资源路径必须输入完整的路径名称/开头并以/结尾例如/foo/
* @param node 文件节点对象如果文件节点不存在则可传入null
* @param block 文件块对象如果文件节点不存在则也应传入null但不会作为文件节点存在与否的判断条件
*/
public KiftdWebDAVResource(String path, Node node, File block) {
this.path = path;
this.node = node;
this.fileBlock = block;
if (this.path != null && this.node != null && this.fileBlock != null) {
isExists = true;
name = this.node.getFileName();
try {
BasicFileAttributes attrs = Files.readAttributes(fileBlock.toPath(), BasicFileAttributes.class);
creation = attrs.creationTime().toMillis();
} catch (IOException e) {
}
} else {
this.name = HttpPathUtil.getResourceName(this.path);
}
}
@Override
public long getLastModified() {
return creation;
}
@Override
public String getLastModifiedHttp() {
return FastHttpDateFormat.formatDate(getLastModified());
}
@Override
public boolean exists() {
return isExists;
}
@Override
public boolean isVirtual() {
return false;
}
@Override
public boolean isDirectory() {
return isDirectory;
}
@Override
public boolean isFile() {
return isExists && !isDirectory;
}
@Override
public boolean delete() {
if (isExists) {
if (isDirectory) {
return true;
}
return fileBlock.delete();
} else {
return false;
}
}
@Override
public String getName() {
return name;
}
@Override
public long getContentLength() {
if (isExists) {
if (isDirectory) {
return -1;
}
return fileBlock.length();
} else {
return 0;
}
}
@Override
public String getCanonicalPath() {
return path;
}
@Override
public boolean canRead() {
if (isExists) {
if (isDirectory) {
return true;
}
return fileBlock.canRead();
} else {
return false;
}
}
@Override
public String getWebappPath() {
return path;
}
@Override
public String getETag() {
// 使用懒加载模式如果第一次调用时该值尚未初始化则临时初始化其值
if (weakETag == null) {
synchronized (this) {
if (weakETag == null) {
long contentLength = getContentLength();
long lastModified = getLastModified();
if ((contentLength >= 0) || (lastModified >= 0)) {
weakETag = "W/\"" + contentLength + "-" + lastModified + "\"";
}
}
}
}
// 已经初始化过了则直接返回
return weakETag;
}
@Override
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
@Override
public String getMimeType() {
return mimeType;
}
@Override
public InputStream getInputStream() {
if (isDirectory) {
// 文件夹没有输入流
return null;
}
try {
return new FileInputStream(fileBlock);
} catch (IOException | NullPointerException e) {
// 文件不存在
return null;
}
}
@Override
public byte[] getContent() {
long len = getContentLength();
if (len > Integer.MAX_VALUE) {
// 文件内容过大
throw new ArrayIndexOutOfBoundsException();
}
if (isDirectory || len < 0) {
// 如果是文件夹或不存在的文件则无内容返回
return null;
}
int size = (int) len;
byte[] result = new byte[size];
int pos = 0;
try (InputStream is = new FileInputStream(fileBlock)) {
while (pos < size) {
int n = is.read(result, pos, size - pos);
if (n < 0) {
break;
}
pos += n;
}
} catch (IOException | NullPointerException e) {
return null;
}
return result;
}
@Override
public long getCreation() {
return creation;
}
@Override
public URL getURL() {
if (isExists) {
try {
return new URI("file:" + path).toURL();
} catch (MalformedURLException | URISyntaxException e) {
// 路径格式有错时也返回null
}
}
return null;
}
@Override
public URL getCodeBase() {
// 不存在解析代码问题因此全部视为普通文件
return getURL();
}
@Override
public WebResourceRoot getWebResourceRoot() {
// 由于使用的是虚拟文件系统因此不基于Servlet容器进行文件操作
return null;
}
@Override
public Certificate[] getCertificates() {
// 不提供签名验证服务
return null;
}
@Override
public Manifest getManifest() {
// 不提供资源关联服务
return null;
}
/**
*
* <h2>获取该资源对应的文件节点</h2>
* <p>
* 当该资源对应的是一个文件时返回文件节点对象
* </p>
*
* @author 青阳龙野(kohgylw)
* @return kohgylw.kiftd.server.model.Node 该资源对应的文件节点对象
* 当该资源不存在或者该资源对应的是一个文件夹时返回null
*/
public Node getNode() {
return node;
}
/**
*
* <h2>获取该资源对应的文件夹</h2>
* <p>
* 当该资源对应的是一个文件夹时返回文件夹对象
* </p>
*
* @author 青阳龙野(kohgylw)
* @return kohgylw.kiftd.server.model.Folder 该资源对应的文件夹对象
* 当该资源不存在或者该资源对应的是一个文件时返回null
*/
public Folder getFolder() {
return folder;
}
}

View File

@ -0,0 +1,108 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kohgylw.kiftd.server.webdav.range;
import java.io.IOException;
import java.io.StringReader;
public class ContentRange {
private final String units;
private final long start;
private final long end;
private final long length;
public ContentRange(String units, long start, long end, long length) {
this.units = units;
this.start = start;
this.end = end;
this.length = length;
}
public String getUnits() {
return units;
}
public long getStart() {
return start;
}
public long getEnd() {
return end;
}
public long getLength() {
return length;
}
/**
* Parses a Content-Range header from an HTTP header.
*
* @param input a reader over the header text
*
* @return the range parsed from the input, or null if not valid
*
* @throws IOException if there was a problem reading the input
*/
public static ContentRange parse(StringReader input) throws IOException {
// Units (required)
String units = HttpParser.readToken(input);
if (units == null || units.length() == 0) {
return null;
}
// Must be followed by SP. Parser is lenient and accepts any LWS here.
// No need for explicit check as something must have terminated the
// token and if that something was anything other than LWS the following
// call to readLong() will fail.
// Start
long start = HttpParser.readLong(input);
// Must be followed by '-'
if (HttpParser.skipConstant(input, "-") == SkipResult.NOT_FOUND) {
return null;
}
// End
long end = HttpParser.readLong(input);
// Must be followed by '/'
if (HttpParser.skipConstant(input, "/") == SkipResult.NOT_FOUND) {
return null;
}
// Length
long length = HttpParser.readLong(input);
// Doesn't matter what we look for, result should be EOF
SkipResult skipResult = HttpParser.skipConstant(input, "X");
if (skipResult != SkipResult.EOF) {
// Invalid range
return null;
}
return new ContentRange(units, start, end, length);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kohgylw.kiftd.server.webdav.range;
enum SkipResult {
FOUND,
NOT_FOUND,
EOF
}

View File

@ -0,0 +1,83 @@
package kohgylw.kiftd.server.webdav.url;
/**
*
* <h2>URL路径处理工具类</h2>
* <p>
* 该工具类用于提供各种URL路径的处理方法所有方法皆为静态因此无需实例化本类直接使用静态方式调用即可 各个方法的具体功能详见方法注释
* </p>
*
* @author 青阳龙野(kohgylw)
* @version 1.0
*/
public class HttpPathUtil {
/**
*
* <h2>获取URL路径中的资源名称</h2>
* <p>
* 该方法将解析出URL路径中包含的逻辑资源名称例如/foo的逻辑资源名称为foo/foo/bar/的逻辑资源名称为bar以此类推
* </p>
*
* @author 青阳龙野(kohgylw)
* @param path URL路径该路径应为以/起始的字符串
* @return java.lang.String 逻辑资源名称当路径格式不正确时例如不以/起始则返回空字符串""
*/
public static String getResourceName(String path) {
if (!isAvailablePath(path)) {
return "";
} else {
if (path.equals("/")) {
// 如果是根目录直接返回根目录名称
return path;
} else {
String currentPath = path;
if (currentPath.endsWith("/")) {
// 如果是文件夹/结尾则先去掉结束的/
currentPath = currentPath.substring(0, currentPath.lastIndexOf('/'));
}
// 取最后一个/后面的名称
int indexOfSep = currentPath.lastIndexOf('/');
return currentPath.substring(indexOfSep + 1);
}
}
}
/**
*
* <h2>获取URL路径中的父路径</h2>
* <p>
* 该方法将解析出URL路径中所包含的逻辑父路径例如/foo的逻辑父路径为//foo/bar/的逻辑父路径为/foo/以此类推
* </p>
*
* @author 青阳龙野(kohgylw)
* @param path URL路径该路径应为以/起始的字符串
* @return java.lang.String 逻辑父路径必定以/结尾
* 当路径格式不正确时例如不以/起始或者路径为/则返回空字符串""
*/
public static String getParentPath(String path) {
if (!isAvailablePath(path) || "/".equals(path)) {
return "";
} else {
String currentPath = path.endsWith("/") ? path.substring(0, path.lastIndexOf('/')) : path;
int index = currentPath.lastIndexOf('/');
return currentPath.substring(0, index + 1);
}
}
/**
*
* <h2>判断是否为合法路径</h2>
* <p>
* 该方法用于判断一个URL路径是否符合1不为null2不为空串3/起始
* </p>
*
* @author 青阳龙野(kohgylw)
* @param path URL路径
* @return boolean 判断结果若满足条件则返回true
*/
private static boolean isAvailablePath(String path) {
return (path != null && !path.isEmpty() && path.startsWith("/"));
}
}

View File

@ -0,0 +1,194 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kohgylw.kiftd.server.webdav.url;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.BitSet;
/**
*
* This class is very similar to the java.net.URLEncoder class.
*
* Unfortunately, with java.net.URLEncoder there is no way to specify to the
* java.net.URLEncoder which characters should NOT be encoded.
*
* This code was moved from MyDefaultServlet.java
*
* @author Craig R. McClanahan
* @author Remy Maucherat
*/
public final class URLEncoder implements Cloneable {
private static final char[] hexadecimal =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
public static final URLEncoder DEFAULT = new URLEncoder();
public static final URLEncoder QUERY = new URLEncoder();
static {
/*
* Encoder for URI paths, so from the spec:
*
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
*
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
*
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
* / "*" / "+" / "," / ";" / "="
*/
// ALPHA and DIGIT are always treated as safe characters
// Add the remaining unreserved characters
DEFAULT.addSafeCharacter('-');
DEFAULT.addSafeCharacter('.');
DEFAULT.addSafeCharacter('_');
DEFAULT.addSafeCharacter('~');
// Add the sub-delims
DEFAULT.addSafeCharacter('!');
DEFAULT.addSafeCharacter('$');
DEFAULT.addSafeCharacter('&');
DEFAULT.addSafeCharacter('\'');
DEFAULT.addSafeCharacter('(');
DEFAULT.addSafeCharacter(')');
DEFAULT.addSafeCharacter('*');
DEFAULT.addSafeCharacter('+');
DEFAULT.addSafeCharacter(',');
DEFAULT.addSafeCharacter(';');
DEFAULT.addSafeCharacter('=');
// Add the remaining literals
DEFAULT.addSafeCharacter(':');
DEFAULT.addSafeCharacter('@');
// Add '/' so it isn't encoded when we encode a path
DEFAULT.addSafeCharacter('/');
/*
* Encoder for query strings
* https://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm
* 0x20 ' ' -> '+'
* 0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, 0x5F, 0x61 to 0x7A as-is
* '*', '-', '.', '0' to '9', 'A' to 'Z', '_', 'a' to 'z'
* Also '=' and '&' are not encoded
* Everything else %nn encoded
*/
// Special encoding for space
QUERY.setEncodeSpaceAsPlus(true);
// Alpha and digit are safe by default
// Add the other permitted characters
QUERY.addSafeCharacter('*');
QUERY.addSafeCharacter('-');
QUERY.addSafeCharacter('.');
QUERY.addSafeCharacter('_');
QUERY.addSafeCharacter('=');
QUERY.addSafeCharacter('&');
}
//Array containing the safe characters set.
private final BitSet safeCharacters;
private boolean encodeSpaceAsPlus = false;
public URLEncoder() {
this(new BitSet(256));
for (char i = 'a'; i <= 'z'; i++) {
addSafeCharacter(i);
}
for (char i = 'A'; i <= 'Z'; i++) {
addSafeCharacter(i);
}
for (char i = '0'; i <= '9'; i++) {
addSafeCharacter(i);
}
}
private URLEncoder(BitSet safeCharacters) {
this.safeCharacters = safeCharacters;
}
public void addSafeCharacter(char c) {
safeCharacters.set(c);
}
public void removeSafeCharacter(char c) {
safeCharacters.clear(c);
}
public void setEncodeSpaceAsPlus(boolean encodeSpaceAsPlus) {
this.encodeSpaceAsPlus = encodeSpaceAsPlus;
}
/**
* URL encodes the provided path using the given character set.
*
* @param path The path to encode
* @param charset The character set to use to convert the path to bytes
*
* @return The encoded path
*/
public String encode(String path, Charset charset) {
int maxBytesPerChar = 10;
StringBuilder rewrittenPath = new StringBuilder(path.length());
ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar);
OutputStreamWriter writer = new OutputStreamWriter(buf, charset);
for (int i = 0; i < path.length(); i++) {
int c = path.charAt(i);
if (safeCharacters.get(c)) {
rewrittenPath.append((char)c);
} else if (encodeSpaceAsPlus && c == ' ') {
rewrittenPath.append('+');
} else {
// convert to external encoding before hex conversion
try {
writer.write((char)c);
writer.flush();
} catch(IOException e) {
buf.reset();
continue;
}
byte[] ba = buf.toByteArray();
for (byte toEncode : ba) {
// Converting each byte in the buffer
rewrittenPath.append('%');
int low = toEncode & 0x0f;
int high = (toEncode & 0xf0) >> 4;
rewrittenPath.append(hexadecimal[high]);
rewrittenPath.append(hexadecimal[low]);
}
buf.reset();
}
}
return rewrittenPath.toString();
}
@Override
public Object clone() {
URLEncoder result = new URLEncoder((BitSet) safeCharacters.clone());
result.setEncodeSpaceAsPlus(encodeSpaceAsPlus);
return result;
}
}

View File

@ -0,0 +1,265 @@
package kohgylw.kiftd.server.webdav.util;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import javax.annotation.Resource;
import org.springframework.stereotype.Component;
import kohgylw.kiftd.server.enumeration.AccountAuth;
import kohgylw.kiftd.server.mapper.FolderMapper;
import kohgylw.kiftd.server.mapper.NodeMapper;
import kohgylw.kiftd.server.model.Folder;
import kohgylw.kiftd.server.model.Node;
import kohgylw.kiftd.server.util.ConfigureReader;
import kohgylw.kiftd.server.util.FileBlockUtil;
import kohgylw.kiftd.server.util.FileNodeUtil;
import kohgylw.kiftd.server.util.FolderUtil;
import kohgylw.kiftd.server.util.LogUtil;
import kohgylw.kiftd.server.util.ServerTimeUtil;
import kohgylw.kiftd.server.util.TextFormateUtil;
import kohgylw.kiftd.server.webdav.exception.UnAuthorizedException;
import kohgylw.kiftd.server.webdav.pojo.KiftdWebDAVResource;
import kohgylw.kiftd.server.webdav.url.HttpPathUtil;
/**
*
* <h2>kiftd虚拟文件系统资源操作工具</h2>
* <p>
* 该工具用于实现基于逻辑路径的各类操作例如根据逻辑路径获取文件文件夹以及封装为KiftdResource的资源对象等 具体功能详见其中各个方法的注释
* 该工具使用Spring IOC容器进行管理
* </p>
*
* @author 青阳龙野(kohgylw)
* @version 1.0
*/
@Component
public class KiftdWebDAVResourcesUtil {
@Resource
private FolderMapper fm;
@Resource
private NodeMapper nm;
@Resource
private FileBlockUtil fbu;
@Resource
private FolderUtil fu;
@Resource
private LogUtil lu;
/**
*
* <h2>根据逻辑路径获取一个文件夹</h2>
* <p>
* 该方法用于根据逻辑路径获得一个文件夹对象当逻辑路径正确时返回其对应的文件夹否则返回null
* 注意此方法在性能上相较于直接根据ID获取文件夹对象要差
* </p>
*
* @author 青阳龙野(kohgylw)
* @param path 逻辑路径必须以/起始例如//foo//foo/bar均合法
* @return kohgylw.kiftd.server.model.Folder
* 逻辑路径对应的文件夹对象如果文件夹不存在或逻辑路径不合法则返回null
*/
public Folder getFolderByPath(String path) {
if (path != null && path.startsWith("/")) {
// 如果以/结尾且不为/则去掉最后的/
String currentPath = path.endsWith("/") && path.length() > 1 ? path.substring(0, path.lastIndexOf('/'))
: path;
currentPath = currentPath.substring(1);
// 从根目录开始按照声明的逻辑路径逐级搜索文件夹
String[] folders = currentPath.split("/");
Folder currentFolder = fm.queryById("root");
for (String f : folders) {
if (!f.isEmpty()) {
Map<String, String> queryMap = new HashMap<>();
queryMap.put("parentId", currentFolder.getFolderId());
queryMap.put("folderName", f);
currentFolder = fm.queryByParentIdAndFolderName(queryMap);
if (currentFolder == null) {
// 如果找到某一级就不存在了则视为找不到该文件夹
return null;
}
}
}
// 如果能顺利找到则返回此文件夹对象
return currentFolder;
}
return null;
}
/**
*
* <h2>根据逻辑路径获取一个文件</h2>
* <p>
* 该方法用于根据逻辑路径获得一个文件节点对象当逻辑路径正确时返回其对应的文件节点否则返回null
* 注意此方法在性能上相较于直接根据ID获取文件节点对象要差
* </p>
*
* @author 青阳龙野(kohgylw)
* @param path 逻辑路径必须以/起始但不能以/结尾例如/foo/foo/bar均合法
* @return kohgylw.kiftd.server.model.Node 逻辑路径对应的文件节点对象如果文件不存在或逻辑路径不合法则返回null
*/
public Node getNodeByPath(String path) {
if (path != null && path.startsWith("/")) {
// 首先根据文件的父路径获取其所在的文件夹
String parentPath = path.substring(0, path.lastIndexOf('/') + 1);
Folder parentFolder = getFolderByPath(parentPath);
if (parentFolder != null) {
// 之后再在这个文件夹中查找到文件
String fileName = path.substring(path.lastIndexOf('/') + 1);
try {
Node file = nm.queryByParentFolderId(parentFolder.getFolderId()).parallelStream()
.filter((e) -> e.getFileName().equals(fileName)).findAny().get();
return file;
} catch (NoSuchElementException e2) {
// 如果没找到则说明逻辑路径有错返回null即可
}
}
}
return null;
}
/**
*
* <h2>根据逻辑路径获取资源对象</h2>
* <p>
* 该方法通过逻辑路径返回一个封装好的资源对象该资源对象是逻辑路径对应的存储在kiftd的一个虚拟文件
* </p>
*
* @author 青阳龙野(kohgylw)
* @param path 逻辑路径必须以/起始例如/foo/foo/bar/均合法
* @return kohgylw.kiftd.server.webdav.pojo.KiftdWebDAVResource 资源对象封装了kiftd虚拟文件系统中文件或文件夹
* 该方法永远不会返回null当路径不正确时将会返回一个不存在的WebDAVResource对象
*/
public KiftdWebDAVResource getResource(String path) {
if (path != null && !path.endsWith("/")) {
// 从文件或文件夹中获取资源对象Windows客户端在请求文件夹时并不会以/结尾因此无法判定是否为文件夹
kohgylw.kiftd.server.model.Node node = getNodeByPath(path);
if (node != null) {
// 如果该逻辑路径确实对应一个文件则返回文件节点的资源对象
return new KiftdWebDAVResource(path, node, fbu.getFileFromBlocks(node));
}
}
// 否则尝试获取文件夹的资源对象
Folder folder = getFolderByPath(path);
return new KiftdWebDAVResource(path, folder);
}
/**
*
* <h2>获取指定逻辑路径下全部文件和文件夹的名称</h2>
* <p>
* 该方法能够获取指定逻辑路径应对应一个文件夹下所有允许指定账户访问的文件和文件夹名称并以字符串数组的形式返回 其中文件夹的名称会以/结尾
* </p>
*
* @author 青阳龙野(kohgylw)
* @param path 逻辑路径必须以/起始应当对应一个文件夹
* @return java.lang.String[] 指定逻辑路径下所有允许指定用户访问的文件和文件夹名称数组其中文件夹名称会以/结尾
* 若指定逻辑路径未指向一个合法的文件夹或该文件夹不允许指定用户访问则会返回一个长度为0的数组
*/
public String[] list(String path, String account) {
Folder f = getFolderByPath(path);
// 这个文件夹存在么
if (f != null) {
// 如果存在它本身是否可以被访问
if (ConfigureReader.instance().accessFolder(f, account)) {
// 如果可以那么它之下有多少文件夹可以被访问
String[] folders = fm.queryByParentId(f.getFolderId()).parallelStream()
.filter((e) -> ConfigureReader.instance().accessFolder(e, account))
.map((e) -> e.getFolderName() + "/").toArray(String[]::new);
String[] files = nm.queryByParentFolderId(f.getFolderId()).parallelStream().map((e) -> e.getFileName())
.toArray(String[]::new);
String[] result = new String[folders.length + files.length];
System.arraycopy(folders, 0, result, 0, folders.length);
System.arraycopy(files, 0, result, folders.length, files.length);
return result;
}
}
return new String[0];
}
/**
*
* <h2>在指定逻辑路径上创建新文件夹</h2>
* <p>
* 该方法将尝试在指定逻辑路径上创建一个新的文件夹其访问限制将设置为默认等级父级文件夹允许的最高访问等级
* 此方法同时还会记录创建文件夹日志如果创建成功的话
* </p>
*
* @author 青阳龙野(kohgylw)
* @param path 逻辑路径必须以/起始例如/foo/bar/均合法
* @param account 操作账户可以传入null匿名访问者或合法的账户名称
* @return kohgylw.kiftd.server.model.Folder 如果创建成功则返回文件夹对象否则返回null
* @throws UnAuthorizedException 操作未被授权
*/
public Folder mkdir(String path, String account) throws UnAuthorizedException {
final String folderName = HttpPathUtil.getResourceName(path);// 逻辑路径中文件夹的预期名称
// 检查逻辑路径是否合法
if (folderName == null || folderName.isEmpty()) {
return null;
}
// 检查名称是否合法
if (!TextFormateUtil.instance().matcherFolderName(folderName) || folderName.indexOf(".") == 0) {
return null;
}
// 判断父文件夹是否存在
final Folder parentFolder = getFolderByPath(HttpPathUtil.getParentPath(path));
if (parentFolder == null) {
return null;
}
// 检查父文件夹否能被账户访问
if (!ConfigureReader.instance().accessFolder(parentFolder, account)) {
throw new UnAuthorizedException();
}
final String parentId = parentFolder.getFolderId();
// 判断账户在父文件夹内是否有创建文件夹权限
if (!ConfigureReader.instance().authorized(account, AccountAuth.CREATE_NEW_FOLDER,
fu.getAllFoldersId(parentId))) {
throw new UnAuthorizedException();
}
// 判断父文件夹中的文件数量是否超限
if (fm.countByParentId(parentFolder.getFolderId()) >= FileNodeUtil.MAXIMUM_NUM_OF_SINGLE_FOLDER) {
return null;
}
// 尝试创建新文件夹
Folder f = new Folder();
if (fm.queryByParentId(parentId).parallelStream().anyMatch((e) -> e.getFolderName().equals(folderName))) {
return null;// 若有重名文件夹则无法继续创建
} else {
f.setFolderName(folderName);
}
f.setFolderConstraint(parentFolder.getFolderConstraint());
f.setFolderId(UUID.randomUUID().toString());
f.setFolderCreationDate(ServerTimeUtil.accurateToDay());
if (account != null) {
f.setFolderCreator(account);
} else {
f.setFolderCreator("匿名用户");
}
f.setFolderParent(parentId);
int i = 0;
while (true) {
try {
final int r = this.fm.insertNewFolder(f);
if (r > 0) {
if (fu.isValidFolder(f)) {
return f;
} else {
return null;
}
}
break;
} catch (Exception e) {
f.setFolderId(UUID.randomUUID().toString());
i++;
}
if (i >= 10) {
break;
}
}
return null;
}
}

View File

@ -1,51 +1,49 @@
Manifest-Version: 1.0
Implementation-Title: kiftd
Implementation-Version: 1.0.35-RELEASE
Built-By: kohgylw
Build-Jdk: 15.0.2
Class-Path: libs/spring-boot-starter-web-2.0.2.RELEASE.jar libs/spring-b
oot-starter-2.0.2.RELEASE.jar libs/spring-boot-2.0.2.RELEASE.jar libs/s
pring-boot-autoconfigure-2.0.2.RELEASE.jar libs/spring-boot-starter-log
ging-2.0.2.RELEASE.jar libs/logback-classic-1.2.3.jar libs/logback-core
-1.2.3.jar libs/log4j-to-slf4j-2.10.0.jar libs/log4j-api-2.10.0.jar lib
s/jul-to-slf4j-1.7.25.jar libs/javax.annotation-api-1.3.2.jar libs/snak
eyaml-1.19.jar libs/spring-boot-starter-json-2.0.2.RELEASE.jar libs/jac
kson-databind-2.9.5.jar libs/jackson-annotations-2.9.0.jar libs/jackson
-core-2.9.5.jar libs/jackson-datatype-jdk8-2.9.5.jar libs/jackson-datat
ype-jsr310-2.9.5.jar libs/jackson-module-parameter-names-2.9.5.jar libs
/spring-boot-starter-tomcat-2.0.2.RELEASE.jar libs/tomcat-embed-core-8.
5.31.jar libs/tomcat-embed-el-8.5.31.jar libs/tomcat-embed-websocket-8.
5.31.jar libs/hibernate-validator-6.0.9.Final.jar libs/spring-web-5.0.6
.RELEASE.jar libs/spring-webmvc-5.0.6.RELEASE.jar libs/spring-aop-5.0.6
.RELEASE.jar libs/spring-context-5.0.6.RELEASE.jar libs/spring-expressi
on-5.0.6.RELEASE.jar libs/gson-2.8.4.jar libs/commons-fileupload-1.3.3.
jar libs/commons-io-2.4.jar libs/mybatis-3.4.1.jar libs/mybatis-spring-
1.3.1.jar libs/h2-1.4.197.jar libs/spring-jdbc-5.0.6.RELEASE.jar libs/s
pring-beans-5.0.6.RELEASE.jar libs/spring-core-5.0.6.RELEASE.jar libs/s
pring-jcl-5.0.6.RELEASE.jar libs/spring-tx-5.0.6.RELEASE.jar libs/mysql
-connector-java-8.0.16.jar libs/protobuf-java-3.6.1.jar libs/zt-zip-1.1
3.jar libs/slf4j-api-1.7.25.jar libs/thumbnailator-0.4.8.jar libs/image
io-jpeg-3.3.2.jar libs/imageio-core-3.3.2.jar libs/imageio-metadata-3.3
.2.jar libs/common-lang-3.3.2.jar libs/common-io-3.3.2.jar libs/common-
image-3.3.2.jar libs/imageio-tiff-3.3.2.jar libs/freemarker-2.3.28.jar
libs/itext-2.1.7.jar libs/bcmail-jdk14-138.jar libs/bcprov-jdk14-138.ja
r libs/bctsp-jdk14-1.38.jar libs/bcprov-jdk14-1.38.jar libs/bcmail-jdk1
4-1.38.jar libs/org.apache.poi.xwpf.converter.pdf-1.0.6.jar libs/org.ap
ache.poi.xwpf.converter.core-1.0.6.jar libs/poi-ooxml-3.10-FINAL.jar li
bs/dom4j-1.6.1.jar libs/xml-apis-1.4.01.jar libs/ooxml-schemas-1.1.jar
libs/xmlbeans-2.3.0.jar libs/stax-api-1.0.1.jar libs/fr.opensagres.xdoc
report.itext.extension-1.0.6.jar libs/jchardet-1.0.jar libs/poi-scratch
pad-3.10-FINAL.jar libs/poi-3.10-FINAL.jar libs/jave-all-deps-3.3.1.jar
libs/jave-core-3.3.1.jar libs/jave-nativebin-win32-3.3.1.jar libs/jave
-nativebin-win64-3.3.1.jar libs/jave-nativebin-linux32-3.3.1.jar libs/j
ave-nativebin-linux64-3.3.1.jar libs/jave-nativebin-osx64-3.3.1.jar lib
s/jave-nativebin-osxm1-3.3.1.jar libs/jave-nativebin-linux-arm32-3.3.1.
jar libs/jave-nativebin-linux-arm64-3.3.1.jar libs/commons-codec-1.11.j
ar libs/flexmark-0.50.44.jar libs/flexmark-util-0.50.44.jar
Implementation-Title: kiftd
Implementation-Version: 1.0.36-SNAPSHOT
Implementation-Vendor-Id: kohgylw
Class-Path: libs/spring-boot-starter-web-2.0.2.RELEASE.jar libs/spring
-boot-starter-2.0.2.RELEASE.jar libs/spring-boot-2.0.2.RELEASE.jar li
bs/spring-boot-autoconfigure-2.0.2.RELEASE.jar libs/spring-boot-start
er-logging-2.0.2.RELEASE.jar libs/logback-classic-1.2.3.jar libs/logb
ack-core-1.2.3.jar libs/log4j-to-slf4j-2.10.0.jar libs/log4j-api-2.10
.0.jar libs/jul-to-slf4j-1.7.25.jar libs/javax.annotation-api-1.3.2.j
ar libs/snakeyaml-1.19.jar libs/spring-boot-starter-json-2.0.2.RELEAS
E.jar libs/jackson-databind-2.9.5.jar libs/jackson-annotations-2.9.0.
jar libs/jackson-core-2.9.5.jar libs/jackson-datatype-jdk8-2.9.5.jar
libs/jackson-datatype-jsr310-2.9.5.jar libs/jackson-module-parameter-
names-2.9.5.jar libs/spring-boot-starter-tomcat-2.0.2.RELEASE.jar lib
s/tomcat-embed-core-8.5.31.jar libs/tomcat-embed-el-8.5.31.jar libs/t
omcat-embed-websocket-8.5.31.jar libs/hibernate-validator-6.0.9.Final
.jar libs/validation-api-2.0.1.Final.jar libs/jboss-logging-3.3.2.Fin
al.jar libs/classmate-1.3.4.jar libs/spring-web-5.0.6.RELEASE.jar lib
s/spring-webmvc-5.0.6.RELEASE.jar libs/spring-aop-5.0.6.RELEASE.jar l
ibs/spring-context-5.0.6.RELEASE.jar libs/spring-expression-5.0.6.REL
EASE.jar libs/gson-2.8.4.jar libs/commons-fileupload-1.3.3.jar libs/c
ommons-io-2.4.jar libs/mybatis-3.4.1.jar libs/mybatis-spring-1.3.1.ja
r libs/h2-1.4.197.jar libs/spring-jdbc-5.0.6.RELEASE.jar libs/spring-
beans-5.0.6.RELEASE.jar libs/spring-core-5.0.6.RELEASE.jar libs/sprin
g-jcl-5.0.6.RELEASE.jar libs/spring-tx-5.0.6.RELEASE.jar libs/mysql-c
onnector-java-8.0.16.jar libs/protobuf-java-3.6.1.jar libs/zt-zip-1.1
3.jar libs/slf4j-api-1.7.25.jar libs/thumbnailator-0.4.8.jar libs/ima
geio-jpeg-3.3.2.jar libs/imageio-core-3.3.2.jar libs/imageio-metadata
-3.3.2.jar libs/common-lang-3.3.2.jar libs/common-io-3.3.2.jar libs/c
ommon-image-3.3.2.jar libs/imageio-tiff-3.3.2.jar libs/freemarker-2.3
.28.jar libs/itext-2.1.7.jar libs/bcmail-jdk14-138.jar libs/bcprov-jd
k14-138.jar libs/bctsp-jdk14-1.38.jar libs/bcprov-jdk14-1.38.jar libs
/bcmail-jdk14-1.38.jar libs/org.apache.poi.xwpf.converter.pdf-1.0.6.j
ar libs/org.apache.poi.xwpf.converter.core-1.0.6.jar libs/poi-ooxml-3
.10-FINAL.jar libs/dom4j-1.6.1.jar libs/xml-apis-1.4.01.jar libs/ooxm
l-schemas-1.1.jar libs/xmlbeans-2.3.0.jar libs/stax-api-1.0.1.jar lib
s/fr.opensagres.xdocreport.itext.extension-1.0.6.jar libs/jchardet-1.
0.jar libs/poi-scratchpad-3.10-FINAL.jar libs/poi-3.10-FINAL.jar libs
/jave-all-deps-2.5.0.jar libs/jave-core-2.5.0.jar libs/commons-loggin
g-api-1.1.jar libs/jave-nativebin-win32-2.5.0.jar libs/jave-nativebin
-win64-2.5.0.jar libs/jave-nativebin-linux32-2.5.0.jar libs/jave-nati
vebin-linux64-2.5.0.jar libs/jave-nativebin-osx64-2.5.0.jar libs/comm
ons-codec-1.11.jar libs/flexmark-0.50.44.jar libs/flexmark-util-0.50.
44.jar
Build-Jdk: 1.8.0_131
Implementation-URL: https://kohgylw.gitee.io
Created-By: Maven Integration for Eclipse
Main-Class: kohgylw.kiftd.mc.MC
Created-By: Maven Integration for Eclipse

View File

@ -1,7 +1,7 @@
#Generated by Maven Integration for Eclipse
#Mon Jul 06 16:32:47 CST 2020
version=1.0.35-RELEASE
groupId=kohgylw
m2e.projectName=kiftd
#Thu Jun 02 06:20:06 CST 2022
m2e.projectLocation=/Users/kohgylw/program/java-workspace/kiftd
m2e.projectName=kiftd
groupId=kohgylw
artifactId=kiftd
version=1.0.36-SNAPSHOT

View File

@ -4,7 +4,7 @@
<groupId>kohgylw</groupId>
<artifactId>kiftd</artifactId>
<version>1.0.35-RELEASE</version>
<version>1.0.36-SNAPSHOT</version>
<packaging>jar</packaging>
<name>kiftd</name>
@ -150,7 +150,7 @@
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-all-deps</artifactId>
<version>2.5.0</version>
<version>3.3.1</version>
</dependency>
<!-- end JAVE -->

Some files were not shown because too many files have changed in this diff Show More