2024年8月

最新技术资源(建议收藏)
https://www.grapecity.com.cn/resources/

前言

在本地使用Excel时,经常会有需要在Excel中添加一些附件文件的需求,例如在Excel中附带一些Word,CAD图等等。同样的,类比到Web端,现在很多人用的在线Excel是否也可以像本地一样实现附件文件的操作呢?答案是肯定的,不过和本地不同的是,Web端不会直接打开附件,而是使用超链接单元格的形式来显示,今天小编将为大家介绍使用前端HTML+JS+CSS技术通过超链接单元格的形式实现在线Excel附件上传、下载和修改的操作。

使用JS实现附件上传

实现的方式分为四个步骤:

1.创建前端页面

2编写暂存附件信息的方法

3.编写附件文件清除的方法

4.编写文件保存和文件加载的方法

1.创建前端页面

核心代码:

<div style="margin-bottom: 8px">

<button id="uploadAttach">上传附件</button>

<button id="removeAttach">清除附件</button>

<button id="fileSaver">文件保存</button>

<button id="loadSubmitFile">加载文件</button>

<button id="loadPackage">打包下载</button>

</div>

<div id="fileOperate" style="visibility: hidden;position: absolute;top: 100px;left: 300px;z-index: 10; background-color: #eee;padding: 16px">

<label for="choseFile">选择文件\</label>

<input type="file" id="choseFile" name="choseFile"/>

<button id="submit">提交</button>

<button id="cancel">取消</button>

</div>

点击上传附件按钮可以把附件上传到对应的单元格,清除附件会清理掉所有已经上传过的附件信息,打包下载会对所有的附件进行统一下载。

2.暂存附件信息方法

这一步起始主要用来设置文件上传之后单元格超链接及tag信息。细心的同学会注意到,这里我注册了一个命令,超链接本身会有一个跳转的行为,写command之后,会阻止这个默认跳转,转去执行对应的命令。注册的命令主要就是用来做附件文件的下载。

核心代码:

function hasAttachFile(sheet,row,col,file){

\*\*

\* 附件文件暂存

\* 这里由于没有服务端,所以我直接存了File对象,但File对象只有在实际使用时才会去获取实际的文件内容。在demo中可行

\* 在实际项目中,需要将file对象上传到文件服务器中

\* 上传完成后tag中的fileInfo应该代表的是文件的访问地址,而不能再是File对象。

\*

sheet.setValue(row,col,file.name)

sheet.setTag(row,col,{

type: hyerlinkType,

fileInfo: file // 实际项目中fileInfo应该为上传完成文件的访问路径

})

sheet.setHyperlink(row, col, {

url: file.name,

linkColor: '#0066cc',

visitedLinkColor: '#3399ff',

drawUnderline: true,

command:'downloadAttachFile',

}, GC.Spread.Sheets.SheetArea.viewport);

}

在这里,我引入了三方组件库FileSaver,在点击超链接单元格时,可以支持当前附件文件的下载。

// 下载文件

spread.commandManager().register("downloadAttachFile",{

canUndo: false,

execute: function(context,options,isUndo){

let sheet = context.getActiveSheet()

let row = sheet.getActiveRowIndex()

let col = sheet.getActiveColumnIndex()

let cellTag = sheet.getTag(row,col)

console.log(sheet,row,col,cellTag)

if(cellTag && cellTag.type==hyerlinkType){

\*\*\*

\* 纯前端demo,文件存在于本地,fileInfo中存储的是File对象,可以直接获取到文件

\* 实际项目中,fileInfo应该是上传到文件服务器上的文件访问地址。

\* 因此这里需要发送请求,先获取文件blob,将获取的blob传递到saveAs的第二个参数中。

\*

saveAs(cellTag.fileInfo,cellTag.fileInfo.name)

}

}

})

3. 附件文件清除

document.getElementById("removeAttach").onclick = function(){

\*\*\*

\* 清除附件

\* 清除附件需要先删除远程文件服务器的文件,之后清除单元格的Tag信息。

\* 这里前端演示demo,只删除了tag。

\* 实际项目中tag中的fileInfo应该是文件上传后的路径

\*

let sheet = spread.getActiveSheet()

let row = sheet.getActiveRowIndex()

let col = sheet.getActiveColumnIndex()

spread.commandManager().execute({

cmd:"removeAttachFile",

sheet,row,col

})
}

4. 文件保存/加载

将文件保存成为JSON结构:

document.getElementById("fileSaver").onclick = function(){

// 保存文件

submitFile = spread.toJSON()

spread.clearSheets()

spread.addSheet(0)

}

加载已保存文件:

document.getElementById("loadSubmitFile").onclick = function(){

// 加载已保存文件

spread.fromJSON(submitFile)

}

实现功能和效果:

在需要在某个单元格中上传附件时,我们可以弹出一个模态框,在模态框中上传文件,点击提交之后,可以对文件做一个暂存,将文件信息存储在单元格的Tag中,点击单元格可以下载文件。

完整代码和在线Demo地址:

https://jscodemine.grapecity.com/share/VHlpNyuP-0CIBNleP5jtyA/

扩展链接:

Spring Boot框架下实现Excel服务端导入导出

项目实战:在线报价采购系统(React +SpreadJS+Echarts)

Svelte 框架结合 SpreadJS 实现纯前端类 Excel 在线报表设计

本文书接上回《
为了给Javaer落地DDD,我们不得不写开源组件
》,欢迎关注公众号(老肖想当外语大佬),获取最新文章更新和DDD框架源码,视频和直播在B站。

背景

我们在《这是DDD建模最难的部分(其实很简单)》一文中介绍了一个关于“用户-角色”的建模过程,当我们讨论“如何分析业务和建模,以在满足需求的前提下,使得需求和模型的边界都清晰且一致”这一话题时,发现很多经验丰富的开发者,总会带着各种各样的顾虑和疑惑,“数据库里的表关系怎么处理”,“关联查询不就解决问题了吗?”,“为啥不能关联查询?”,“既然有了某某Id,说明它们有关系啊,你为啥说边界明确?”。
我就在想,这里的问题到底在哪里,为什么我们觉得理所当然的想法,仍然有很多人会觉得困惑,经过和我们团队伙伴们深入探讨,我觉得我们找到了问题所在。

知识的诅咒

我记得在《倚天屠龙记》中有这么一幕,张三丰现场传授张无忌太极剑法,教完之后,问张无忌好几遍:“还记得多少?”,张无忌最后说:“我已经把所有的全忘记了!”,张三丰很高兴:“好,你可以上了。”
回到我们软件设计的场景,我们经验丰富的工程师,总是会深思熟虑,会考虑性能够不够?模型怎么存到表里?表结构是否合理?这里应该一对一关系还是一对多?又或者应该是多对多?这一系列的问题使得大家无法集中精力思考业务的本质是什么,过早地把注意力放在了技术上,在跟业务专家热烈沟通客户场景的时候,你的脑海里却满满的SQL语句怎么写。
我想,这大概就是
知识的诅咒
吧,背负着沉重的心智负担,大概率也做不出更准确的判断。

忘掉数据库

现在假设科技已经发展到了非常顶级的水准,我们具备了如下能力:
  1. 应用程序的内存无限大;
  2. 应用程序内存永远不会丢失;
那么,我们还需要数据库吗?我想,应该不需要了,我们代码会是怎么样的?是不是在内存中构造出模型的实例添加到它的集合容器中,就可以了?
假如不再需要数据库了,你建模的时候是不是可以忘掉数据库,把模型看作是一个个独立的类型实例即可?你的建模思路是否会发生一些变化?那么在这个背景下,我们重新审视“领域的边界”这个概念。
假如我们仍然使用之前的文章中提到的准则:“
聚合之间不存在相互引用
”,那么你设计出来的结果是不是就会像我们之前推演的那样,像下图一样:

如果你仍然有疑惑,我把具体的类图也添加进来,你是不是就一目了然了:

回到现实

当然,现实是我们的科技并没有像上面设想的那样发达,我们最终还是要将模型数据存储到数据库的,因此,我们需要ORM框架来帮助我们解决模型的“存取”问题,注意这里我用到的是“存取”,不包含搜索,搜索的事情,我们可以用更加灵活的解决方案,这涉及到一种叫做CQRS的模式,这又是另一个故事了。
假如我们有一个很强大的ORM,可以帮助我们根据Id,取出模型,我们操作完模型,ORM再帮我们“Save”进数据库,我们不需要关心这里面它到底做了什么,那么是不是这个ORM也可以帮助我们摆脱“数据库知识”的诅咒,让我们在建模的时候专注需求和模型?

结论是什么

基于上面的推导,我认为有如下结论:
  1. 数据库知识,会成为分析需求和建模时候的心智负担;
  2. 一个功能强大的ORM,有利于帮助工程师摆脱“数据库知识”的心智负担;
  3. 分析需求的时候,只需要关心需求和模型即可;
那么,你对上面的结论有什么看法?你在用什么样的ORM?你参与的项目的代码组织方式,是否让你可以专注业务?欢迎在文章评论区友善地讨论,也欢迎关注我的公众号(老肖想当外语大佬)以获得最新的更新。

后续

下一篇,我将介绍一种能够在各个角色间建立共鸣的建模沟通方法,以使得我们的建模思维可以落地和复制,敬请期待。

Microsoft 将于 2024 年 8 月 20 日举办免费的
.NET Conf: Focus on AI
。该虚拟活动为开发人员提供了如何集成 .NET 和 AI 以增强应用程序开发和用户体验的见解,其中包括专家和行业领导者的会议。

为什么你不应该错过这个活动?

.NET Conf: Focus 系列由全年举行的小型现场活动组成。8 月 20 日,我们将深入探讨 AI 世界,并探索如何 .NET 开发人员可以利用 AI 库和功能来构建更智能的应用程序并提高工作效率,该活动是“.NET Conf: Focus”系列的一部分,将由 Microsoft 团队和社区专家主持会议。Seth Juarez 和 Maria Naggaga Nakanwagi 将发表题为“.NET + AI 的状态”的主题演讲,讨论 AI 的基础知识、大型语言模型 (LLM) 以及 .NET 生态系统中 AI 技术的最新进展。它将展示来自行业专家的见解和来自 Milvus 和 MongoDB 等合作伙伴以及 H&R Block 等公司的实际示例。

主题演讲:.NET + AI 的状态

在引人入胜的主题演讲中聆听 Seth Juarez 和 Maria Naggaga Nakanwagi 的演讲,他们将分享 AI 的基础知识、大型语言模型 (LLM) 以及 .NET 生态系统中 AI 技术的最新进展。了解 .NET 如何在集成这些技术方面处于领先地位。详细的议程参看:
https://focus.dotnetconf.net/agenda

会议内容

  • .NET 应用程序中开始使用 AI
    :了解将 AI 集成到 .NET 应用程序。本次会议将带您从一个空洞的项目转变为一个完全集成的 AI 解决方案。
  • .NET Aspire 和语Semantic kernel
    更好地结合在一起
    :了解 .NET Aspire 和Semantic kernel之间的协同作用,以构建智能应用程序。
  • 使用 Blazor 和 .NET 的交互式 AI:
    了解如何使用 Blazor 和 .NET 构建交互式 AI 驱动的 Web 应用程序,包括智能组件和聊天机器人用户界面。
  • 在 .NET 中导航 AI 模型的世界:从本地开发到云
    :探索不同的 AI 模型并了解如何将它们集成到您的 .NET 应用程序。
  • 使用 .NET、AI 和 Azure SQL 处理数据:
    了解如何使用 SQL Server 或 Azure SQL 构建检索增强生成解决方案。
  • 将语义搜索功能与 .NET 和 Azure 集成
    :了解 Milvus 如何使用向量嵌入来实现强大的语义搜索功能。
  • 从将生成式 AI 应用于具有 .NET 和 Azure 的应用中吸取的经验
    教训:听取 H&R Block 关于使用 .NET 和 Azure 构建和改进 AI 助手的真实经验。

.NET Conf: Focus on AI 是一个免费的、为期一天的直播活动,你不想错过。请于 2024 年 8 月 20 日收看,提出问题,并了解如何借助 AI 使你的 NET 应用程序更智能。会议相关的PPT 已经开始提前上传到github:
https://github.com/dotnetConf/FocusOnAI_24

开心一刻

今天逛街碰到街头采访,一上来就问我敏感话题

主持人:小哥哥,你单身吗

我:是啊

主持人:你找女朋友的话,是想找一个小奶猫呢,还是小野猫呢

我沉思了一下,叹气道:如果可以的话,我想找个人,而且是女人

开心一刻

上传文件

基于
commons-vfs2
实现文件到
FTP
服务器的上传,pom.xml 如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.qsl</groupId>
    <artifactId>ftp-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-vfs2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>commons-net</groupId>
            <artifactId>commons-net</artifactId>
            <version>3.9.0</version>
        </dependency>
    </dependencies>
</project>

application.yml

server:
  port: 8080

app:
  ftp:
    host: ftp_ip
    userName: ftp账号
    password: ftp账号的密码
    port: 21
    protocol: ftp
    baseDir: 账号基础目录

FtpConfig.java

/**
 * FTP 配置
 * @author 青石路
 */
@Configuration
@ConfigurationProperties(prefix = "app.ftp")
public class FtpConfig {

    private String host;
    private String userName;
    private String password;
    private Integer port;
    private String protocol;
    private String baseDir;

    @Bean("fptUri")
    public URI fptUri() throws URISyntaxException {
        return new URI(protocol, userName+":"+password, host,
                port, baseDir, null, null);
    }

    @Bean
    public FileSystemOptions fileSystemOptions() {
        FileSystemOptions opts = new FileSystemOptions();
        FtpFileSystemConfigBuilder builder = FtpFileSystemConfigBuilder.getInstance();
        builder.setControlEncoding(opts, "UTF-8");
        builder.setConnectTimeout(opts, 5000);
        builder.setUserDirIsRoot(opts, true);
        builder.setPassiveMode(opts, true);
        return opts;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getPort() {
        return port;
    }

    public void setPort(Integer port) {
        this.port = port;
    }

    public String getProtocol() {
        return protocol;
    }

    public void setProtocol(String protocol) {
        this.protocol = protocol;
    }

    public String getBaseDir() {
        return baseDir;
    }

    public void setBaseDir(String baseDir) {
        this.baseDir = baseDir;
    }
}

FileUploadManager.java 完成上传

/**
 * 文件上传 manager
 * @author 青石路
 */
@Component
public class FileUploadManager {

    private static final Logger LOGGER = LoggerFactory.getLogger(FileUploadManager.class);
    private static final FileSystemManager systemManager;

    @Autowired
    @Qualifier("fptUri")
    private URI fptUri;
    @Autowired
    private FileSystemOptions fileSystemOptions;

    static {
        try {
            systemManager = VFS.getManager();
        } catch (FileSystemException e) {
            throw new RuntimeException(e);
        }
    }

    public boolean uploadFileToSftp(File file, String fileName) {
        try {
            FileObject srcObject = systemManager.resolveFile(file.getParentFile(), file.getName());
            FileObject destObjectDir = systemManager.resolveFile(fptUri.toString(), fileSystemOptions);
            FileObject destObject = systemManager.resolveFile(destObjectDir, fileName);
            destObject.copyFrom(srcObject, Selectors.SELECT_SELF);
            return true;
        } catch (FileSystemException e) {
            LOGGER.error("文件:{} 上传SFTP失败,异常:", file.getAbsoluteFile(), e);
            return false;
        }
    }
}

FileUploadController.java

/**
 * 文件上传 controller
 * @author 青石路
 */
@RestController
@RequestMapping("file")
public class FileUploadController {

    private static final Logger LOGGER = LoggerFactory.getLogger(FileUploadController.class);

    @Autowired
    private FileUploadManager fileUploadManager;

    @GetMapping("upload")
    public boolean upload(@RequestParam("fileName") String fileName) {
        long ramdomLong = ThreadLocalRandom.current().nextLong();
        File file = new File(System.getProperty("java.io.tmpdir") + File.separator + ramdomLong + ".txt");
        LOGGER.info("localFile:{}", file.getAbsoluteFile());
        boolean uploadResult = false;
        try {
            Files.write(file.toPath(), String.valueOf(ramdomLong).getBytes(StandardCharsets.UTF_8));
            uploadResult = fileUploadManager.uploadFileToSftp(file, fileName);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                Files.deleteIfExists(file.toPath());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return uploadResult;
    }
}

完整代码:
ftp-demo

FTP 服务器目录最初情况如下

FTP_最初情况

/idg
下没有任何目录和文件;启动后调用接口

http://localhost:8080/file/upload?fileName=hello.txt

即可完成文件的上传;
fileName
参数表示上传到 FTP 服务器上的文件名

upload_ok

true
表示上传成功,FTP 服务器上即可看到
hello.txt

upload_ok_ftp

file
目录也被自动创建了,一切都是那么的顺利

上传失败:553

一个不小心把 FTP 服务器上
file
目录给删了,但内心一点都不慌,再上传一次呗,正好我也是这么干的;正当我以为会正常上传的时候,意外来了

org.apache.commons.vfs2.FileSystemException: Could not copy "file:///C:/Users/qsl/AppData/Local/Temp/6456333409667879871.txt" to "ftp://test:***@192.168.2.118/idg/file/hello.txt".
	at org.apache.commons.vfs2.provider.AbstractFileObject.copyFrom(AbstractFileObject.java:303)
	at com.qsl.manager.FileUploadManager.uploadFileToSftp(FileUploadManager.java:47)
	at com.qsl.web.FileUploadController.upload(FileUploadController.java:39)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1072)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:965)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:529)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:623)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:209)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:168)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:481)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:130)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:928)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1794)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:745)
Caused by: org.apache.commons.vfs2.FileSystemException: Could not write to "ftp://test:***@192.168.2.118/idg/file/hello.txt".
	at org.apache.commons.vfs2.provider.AbstractFileObject.getOutputStream(AbstractFileObject.java:1280)
	at org.apache.commons.vfs2.provider.DefaultFileContent.buildOutputStream(DefaultFileContent.java:540)
	at org.apache.commons.vfs2.provider.DefaultFileContent.getOutputStream(DefaultFileContent.java:406)
	at org.apache.commons.vfs2.provider.DefaultFileContent.getOutputStream(DefaultFileContent.java:394)
	at org.apache.commons.vfs2.provider.DefaultFileContent.write(DefaultFileContent.java:815)
	at org.apache.commons.vfs2.provider.DefaultFileContent.write(DefaultFileContent.java:830)
	at org.apache.commons.vfs2.util.FileObjectUtils.writeContent(FileObjectUtils.java:203)
	at org.apache.commons.vfs2.provider.AbstractFileObject.copyFrom(AbstractFileObject.java:298)
	... 52 common frames omitted
Caused by: org.apache.commons.vfs2.FileSystemException: Cant open output connection for file "ftp://test:***@192.168.2.118/idg/file/hello.txt". Reason: "553 Can't open that file: No such file or directory
".
	at org.apache.commons.vfs2.FileSystemException.requireNonNull(FileSystemException.java:87)
	at org.apache.commons.vfs2.provider.ftp.FtpFileObject.doGetOutputStream(FtpFileObject.java:519)
	at org.apache.commons.vfs2.provider.AbstractFileObject.getOutputStream(AbstractFileObject.java:1276)
	... 59 common frames omitted

553 Can't open that file: No such file or directory 是什么鬼?莫非是
file
目录不存在的原因?试着手动创建
file
目录,再调用接口上传文件,文件正常上传!

我们来分析下,最初的时候
file
目录是不存在的,但自动创建了,文件也正常上传了,然后我们手动删除
file
目录后,上传文件失败,手动补上
file
目录后,上传又正常了,这说明
file
目录被缓存了呀,对不对?最初的时候,缓存是空的,第一次上传的时候,
vfs2
会判断 FTP 服务器上是否存在
file
目录,不存在则创建并进行缓存,那么下次上传的时候,在缓存中找到了
file
目录,那么就直接上传文件了,而不用去判断 FTP 服务器上是否有
file
目录(没有则创建);缓存的作用就很明显了,减少了一次目录是否存在的网络请求,进而提高效率;当然这只是我们的猜想,是否真的存在缓存,看源码肯定是最直观的,入口代码

FileObject destObjectDir = systemManager.resolveFile(fptUri.toString(), fileSystemOptions);

就不带你们详细去跟源码了,
debug
跟源码,我相信你们也很容易找到
AbstractFileSystem#resolveFile(final FileName name, final boolean useCache)

private synchronized FileObject resolveFile(final FileName name, final boolean useCache)
		throws FileSystemException {
	if (!rootName.getRootURI().equals(name.getRootURI())) {
		throw new FileSystemException("vfs.provider/mismatched-fs-for-name.error", name, rootName,
				name.getRootURI());
	}

	// imario@apache.org ==> use getFileFromCache
	FileObject file;
	if (useCache) {
		file = getFileFromCache(name);
	} else {
		file = null;
	}

	if (file == null) {
		try {
			file = createFile((AbstractFileName) name);
		} catch (final Exception e) {
			throw new FileSystemException("vfs.provider/resolve-file.error", name, e);
		}

		file = decorateFileObject(file);

		// imario@apache.org ==> use putFileToCache
		if (useCache) {
			putFileToCache(file);
		}
	}

	/**
	 * resync the file information if requested
	 */
	if (getFileSystemManager().getCacheStrategy().equals(CacheStrategy.ON_RESOLVE)) {
		file.refresh();
	}
	return file;
}

如何修复

首先我们讨论下:要不要修?

手动误删目录,这种情况是非常少的,就拿我们的生产来讲,2020 到现在,从未出现过该问题,如果因为这种极小概率的事件去放弃缓存带来的性能提升,得不偿失,所以我是不推荐修改的,而实际上经过讨论后也决定不去修改;不过话说回来,如果上传压力很小,网络又非常稳定、快速,也就说缓存带来的性能提升可以忽略,那这个时候高可用的优先级就更高了,此时就可以考虑修了,那怎么修了,我这里提供两种方案

  1. 禁用缓存

    既然
    vfs2
    有缓存(默认是启用的),应该有开关来禁用它,我就不给你们打哑谜了,直接修改
    FileSystemManager
    ,换成其子类
    StandardFileSystemManager

    private static final StandardFileSystemManager systemManager;
    
    static {
    	try {
    		systemManager = new StandardFileSystemManager();
            // 禁用缓存
    		systemManager.setFilesCache(new NullFilesCache());
    		systemManager.init();
    	} catch (FileSystemException e) {
    		throw new RuntimeException(e);
    	}
    }
    

    禁用cache_代码前后区别

    你们也许会问,我是怎么知道可以这么调整的,你们跟一下
    VFS.getManager()
    就知道了,最终会看到

    // managerClassName: org.apache.commons.vfs2.impl.StandardFileSystemManager
    private static FileSystemManager createFileSystemManager(final String managerClassName) throws FileSystemException {
    	try {
    		// Create instance
    		final Class<?> mgrClass = Class.forName(managerClassName);
    		final FileSystemManager mgr = (FileSystemManager) mgrClass.newInstance();
    
    		try {
    			// Initialize
    			final Method initMethod = mgrClass.getMethod("init", (Class[]) null);
    			initMethod.invoke(mgr, (Object[]) null);
    		} catch (final NoSuchMethodException ignored) {
    			/* Ignore; don't initialize. */
    		}
    
    		return mgr;
    	} catch (final InvocationTargetException e) {
    		throw new FileSystemException("vfs/create-manager.error", managerClassName, e.getTargetException());
    	} catch (final Exception e) {
    		throw new FileSystemException("vfs/create-manager.error", managerClassName, e);
    	}
    }
    

    通过反射调用了
    StandardFileSystemManager
    的构造方法和
    init
    方法,与我们的

    systemManager = new StandardFileSystemManager();
    systemManager.init();
    

    是不是有异曲同工之妙?(你们猜的没错,我们的实现正是抄自于
    vfs2


    有点东西
  2. 异常弥补

    不禁用缓存,还是保留默认的开启,只是当异常的时候,捕获它,然后去创建目录,然后再上传一次

    public boolean uploadFileToSftp(File file, String fileName, boolean isRetry) {
    	String destDir = fptUri.toString();
    	try {
    		FileObject srcObject = systemManager.resolveFile(file.getParentFile(), file.getName());
    		FileObject destObjectDir = systemManager.resolveFile(destDir, fileSystemOptions);
    		FileObject destObject = systemManager.resolveFile(destObjectDir, fileName);
    		destObject.copyFrom(srcObject, Selectors.SELECT_SELF);
    		return true;
    	} catch (FileSystemException e) {
    		if (isRetry) {
    			try {
    				LOGGER.info("创建目录{}开始", destDir);
    				FileObject destObjectDir = systemManager.resolveFile(destDir, fileSystemOptions);
    				destObjectDir.createFolder();
    				LOGGER.info("创建目录{}开始", destDir);
    			} catch (FileSystemException ex) {
    				LOGGER.error("创建目录失败,异常:", ex);
    			}
    			return uploadFileToSftp(file, fileName, false);
    		}
    		LOGGER.error("文件:{} 上传SFTP失败,异常:", file.getAbsoluteFile(), e);
    		return false;
    	}
    }
    

    异常弥补_代码前后区别

    既保留了缓存,也解决了目录误删的问题,就问你们服不服?


    愣着干啥,鼓掌

总结

vfs2
是有缓存的,如果不小心把 FTP 目录删除了,上传会失败并提示

553 Can't open that file: No such file or directory

可以通过手动补目录的方式就行处理,当然也可以通过重启服务来解决,但这两种都不是通过代码来解决的,可用性很低;通过代码的方式来解决,有两种方法

  1. 禁用
    vfs2
    缓存,但会降低性能,可用但不推荐

  2. 异常弥补,既保留了缓存,也解决了目录误删的问题,可以用也推荐

    异常捕获后用来做流程控制,条件控制,不太规范

就在上周,发完那篇文章之后不久,我就有幸获得了 GitHub Models 服务公测的访问权限,所以就体验了一下 Playground 聊天功能。

起初,我以为这是“微软菩萨”降临,但玩了一圈下来,发现实际效果并没有那么惊艳,还没上周热门的开源项目有趣。例如,基于 WebRTC 的文件传输平台 ShareDrop,只需打开网页,就能在局域网或互联网上安全地跨设备传文件。而可自建支持目标检测和安全报警的视频监控平台 Frigate 和自托管的个人财务管理平台 firefly-iii,这些开源项目才是真正的“开源菩萨”。

更令人惊喜的还有,一周迅速涨了 1w Star 的实时换脸与深度伪造技术 Deep-Live-Cam,以及 Star 持续稳定增长的 PostgreSQL 无代码平台 Teable。

  • 本文目录
    • 1. 开源新闻
      • 1.1 上手体验 GitHub Models
    • 2. 开源热搜项目
      • 2.1 基于 WebRTC 的跨设备文件分享:ShareDrop
      • 2.2 集成 AI 的视频监控平台:Frigate
      • 2.3 PostgreSQL 无代码平台:Teable
      • 2.4 自托管的个人财务管理平台:firefly-iii
      • 2.5 实时换脸与深度伪造技术:Deep-Live-Cam
    • 3. HelloGitHub 热评
      • 3.1 Python 的热重载工具:jurigged
      • 3.2 通用的 Git 凭据管理器:git-credential-manager
    • 4. 结尾

1.1 上手体验 GitHub Models

GitHub Models 的整体使用体验不尽如人意,具体在聊天功能(Playground)方面,我个人觉得和 Poe 还相差甚远:

  1. 可选模型有限,仅支持文字输入
  2. 免费配额限制较少,且无余额提醒
  3. 服务不稳定,时常出现部分回复未加载、卡顿等问题

关于额度,不同模型分为 Low、High、Embedding 三种配额,最后附上一张 Rate limits 图:

2. 开源热搜项目

2.1 基于 WebRTC 的跨设备文件分享:ShareDrop

主语言:JavaScript

Star:9.6k

周增长:500

这是一款受 Apple AirDrop 启发的开源 Web 应用,利用 WebRTC 技术实现了安全的点对点(P2P)文件传输。用户无需上传文件至服务器或安装客户端,只需打开网页,即可在局域网、互联网和跨设备间轻松、安全地共享文件。

GitHub 地址→
github.com/szimek/sharedrop

2.2 集成 AI 的视频监控平台:Frigate

主语言:Python

Star:16.9k

周增长:400

该项目是专为 Home Assistant 设计的监控视频录制和保存平台(NVR),它基于 OpenCV 和 TensorFlow,可实时分析 IP 摄像头的视频流,具备目标检测、运动检测和安全报警等功能,并能根据设定的规则,自动保留检测到物体的视频片段。

GitHub 地址→
github.com/blakeblackshear/frigate

2.3 PostgreSQL 无代码平台:Teable

主语言:TypeScript

Star:9.5k

周增长:1.2k

该项目是基于 PostgreSQL 构建的无代码数据库应用平台,将电子表格的直观操作界面和企业级数据库功能相结合。它提供简洁易用的电子表格式界面,简化了数据库应用开发,无需写代码即可创建企业级数据分析工具。作为 Airtable 的开源替代品,它支持 SQL、数据视图、排序/过滤/聚合/格式化、分组和数学公式等功能,适用于运营和活动数据分析、客户关系管理、CMS 数据接口等场景。

GitHub 地址→
github.com/teableio/teable

2.4 自托管的个人财务管理平台:firefly-iii

主语言:PHP

Star:15.2k

周增长:100

这是一款支持自托管的免费个人财务管理平台,帮助用户跟踪和管理自己的收入/支出,清楚掌握财务状况。它采用 PHP+Vue 构建,支持多种货币、自定义预算、用户认证、财务报表和 REST JSON API 等功能。还可通过 Docker 等方式自行部署,无需将财务数据上传至云端。

GitHub 地址→
github.com/firefly-iii/firefly-iii

2.5 实时换脸与深度伪造技术:Deep-Live-Cam

主语言:Python

Star:14.2k

周增长:13k

该项目利用 AI 技术实现了视频和图片的实时人脸替换。用户仅需提供一张图片,即可将选定的人脸替换到目标视频或图片上,生成栩栩如生的深度伪造效果。它采用 Python 语言和 ONNX、ffmpeg 等库构建,并通过 CUDA 和 CoreML 实现 GPU 加速,提供了友好的界面,不仅操作简单,还内置了防止不当使用的检查机制,确保生成的内容合法合规。

GitHub 地址→
github.com/hacksider/Deep-Live-Cam

3. HelloGitHub 热评

在本章节中,我们将分享本周 HelloGitHub 网站上的热门开源项目,欢迎与我们分享你上手这些开源项目后的使用体验。

3.1 Python 的热重载工具:jurigged

主语言:Python

这是一个专为 Python 提供热重载功能的库,它支持在程序运行时修改和更新 Python 代码,无需重启程序。

# Loop over a function
jurigged --loop function_name script.py
jurigged --loop module_name:function_name script.py

# Only stop on exceptions
jurigged --xloop function_name script.py

项目详情→
hellogithub.com/repository/968788b867c5433da0b3461a035b162e

3.2 通用的 Git 凭据管理器:git-credential-manager

主语言:C#

这是一个基于 .NET 开发的 Git 凭证存储和管理工具。它开箱即用、无需额外操作,使用 git 命令时,工具会自动引导完成登录,后续无需再次登录,轻松解决对远程 Git 仓库进行操作时遇到的需要登录和认证失败等问题,支持 GitHub、Bitbucket 和 GitLab 等平台。

项目详情→
hellogithub.com/repository/551688d3f0aa44d18afc767267cfc417

4. 结尾

以上就是本期「GitHub 热点速览」的全部内容,希望你能够在这里找到自己感兴趣的开源项目,如果你有其他好玩、有趣的 GitHub 开源项目想要分享,欢迎来
HelloGitHub
与我们交流和讨论。

往期回顾