Spring Boot 实战 - 打造私人云盘
阅读文本大概需要 5 分钟。
前言
最近在做工作流的事情,正好有个需求,要添加一个附件上传的功能,曾找过不少上传插件,都不是特别满意。无意中发现一个很好用的开源web文件管理器插件 elfinder,功能比较完善,社区也很活跃,还方便二次开发。
环境搭建
| 软件 | 地址 |
|---|---|
| SpringBoot | https://spring.io/projects/spring-boot/ |
| elFinder | https://studio-42.github.io/elFinder/ |
项目截图
周末抽时间做了一个简单的案例,希望对大家有所帮助,下面是简单的项目截图。
项目功能
在线新建目录、文件、附件上传、下载、预览、在线打包,图片在线裁剪、编辑,实现列表试图、图标视图等等一些列功能。
项目配置
项目基于 SpringBoot 注解配置实现,在第三方插件进行二次开发。
application.properties 配置:
# 执行类,内部调用,实现前端相关功能file-manager.command=com.itstyle.cloud.common.elfinder.commandfile-manager.thumbnail.width=80file-manager.volumes[0].Node=file-manager.volumes[0].source=fileSystemfile-manager.volumes[0].alias=file
# 文件存放目录,可以自定义file-manager.volumes[0].path=D:/cloudFilefile-manager.volumes[0]._default=truefile-manager.volumes[0].locale=file-manager.volumes[0].constraint.locked=falsefile-manager.volumes[0].constraint.readable=truefile-manager.volumes[0].constraint.writable=trueElfinderConfiguration 读取配置:
@Component@ConfigurationProperties(prefix="file-manager") //接收application.properties中的file-manager下面的属性public class ElfinderConfiguration { private Thumbnail thumbnail; private String command; private List<Node> volumes; private Long maxUploadSize = -1L; //省略部分代码}elfinderStorageFactory 初始化 基础Bean:
@Configurationpublic class ElFinderConfig {
@Autowired private ElfinderConfiguration elfinderConfiguration;
@Bean(name = "commandFactory") public CommandFactory getCommandFactory() { CommandFactory commandFactory = new CommandFactory(); commandFactory.setClassNamePattern(elfinderConfiguration.getCommand()+".%sCommand"); return commandFactory; }
@Bean(name = "elfinderStorageFactory") public ElfinderStorageFactory getElfinderStorageFactory() { DefaultElfinderStorageFactory elfinderStorageFactory = new DefaultElfinderStorageFactory(); elfinderStorageFactory.setElfinderStorage(getElfinderStorage()); return elfinderStorageFactory; }
@Bean(name = "elfinderStorage") public ElfinderStorage getElfinderStorage() { DefaultElfinderStorage defaultElfinderStorage = new DefaultElfinderStorage(); // creates thumbnail DefaultThumbnailWidth defaultThumbnailWidth = new DefaultThumbnailWidth(); defaultThumbnailWidth.setThumbnailWidth(elfinderConfiguration.getThumbnail().getWidth().intValue()); // creates volumes, volumeIds, volumeLocale and volumeSecurities Character defaultVolumeId = 'A'; List<Node> elfinderConfigurationVolumes = elfinderConfiguration.getVolumes(); List<Volume> elfinderVolumes = new ArrayList<>(elfinderConfigurationVolumes.size()); Map<Volume, String> elfinderVolumeIds = new HashMap<>(elfinderConfigurationVolumes.size()); Map<Volume, Locale> elfinderVolumeLocales = new HashMap<>(elfinderConfigurationVolumes.size()); List<VolumeSecurity> elfinderVolumeSecurities = new ArrayList<>(); // creates volumes for (Node elfinderConfigurationVolume : elfinderConfigurationVolumes) final String alias = elfinderConfigurationVolume.getAlias(); final String path = elfinderConfigurationVolume.getPath(); final String source = elfinderConfigurationVolume.getSource(); final String locale = elfinderConfigurationVolume.getLocale(); final boolean isLocked = elfinderConfigurationVolume.getConstraint().isLocked(); final boolean isReadable = elfinderConfigurationVolume.getConstraint().isReadable(); final boolean isWritable = elfinderConfigurationVolume.getConstraint().isWritable(); // creates new volume Volume volume = VolumeSources.of(source).newInstance(alias, path); elfinderVolumes.add(volume); elfinderVolumeIds.put(volume, Character.toString(defaultVolumeId)); elfinderVolumeLocales.put(volume, LocaleUtils.toLocale(locale)); // creates security constraint SecurityConstraint securityConstraint = new SecurityConstraint(); securityConstraint.setLocked(isLocked); securityConstraint.setReadable(isReadable); securityConstraint.setWritable(isWritable); // creates volume pattern and volume security final String volumePattern = Character.toString(defaultVolumeId) + ElFinderConstants.ELFINDER_VOLUME_SERCURITY_REGEX; elfinderVolumeSecurities.add(new DefaultVolumeSecurity(volumePattern, securityConstraint)) // prepare next volumeId character defaultVolumeId++; } defaultElfinderStorage.setThumbnailWidth(defaultThumbnailWidth); defaultElfinderStorage.setVolumes(elfinderVolumes); defaultElfinderStorage.setVolumeIds(elfinderVolumeIds); defaultElfinderStorage.setVolumeLocales(elfinderVolumeLocales); defaultElfinderStorage.setVolumeSecurities(elfinderVolumeSecurities); return defaultElfinderStorage; }}CloudDiskController 控制层实现:
@Controller@RequestMapping("elfinder/connector")public class CloudDiskController { private static final Logger logger = LoggerFactory.getLogger(CloudDiskController.class); public static final String OPEN_STREAM = "openStream"; public static final String GET_PARAMETER = "getParameter"; @Resource(name = "commandFactory") private ElfinderCommandFactory elfinderCommandFactory;
@Resource(name = "elfinderStorageFactory") private ElfinderStorageFactory elfinderStorageFactory;
@RequestMapping public void connector(HttpServletRequest request, final HttpServletResponse response) throws IOException { try { response.setCharacterEncoding("UTF-8"); request = processMultipartContent(request); } catch (Exception e) { throw new IOException(e.getMessage()); } String cmd = request.getParameter(ElFinderConstants.ELFINDER_PARAMETER_COMMAND); ElfinderCommand elfinderCommand = elfinderCommandFactory.get(cmd); try { final HttpServletRequest protectedRequest = request; elfinderCommand.execute(new ElfinderContext() { @Override public ElfinderStorageFactory getVolumeSourceFactory() { return elfinderStorageFactory; }
@Override public HttpServletRequest getRequest() { return protectedRequest; }
@Override public HttpServletResponse getResponse() { return response; } }); } catch (Exception e) { logger.error("Unknown error", e); } } //省略部分代码}最后,前端页面引入:
<div id="elfinder"></div>
<script type="text/javascript" charset="utf-8"> window.onload = function() { elFinder.prototype.loadCss('/elfinder/jquery-ui-1.12.1.custom/jquery-ui.css'); $('#elfinder').elfinder({ url: '/elfinder/connector', lang: 'zh_CN', height: window.innerHeight - 20, commandsOptions: { edit: { editors: [ { info: { name: '编辑', urlAsContent: false },
// ACE Editor // `mimes` is not set for support everything kind of text file load: function(textarea) { var self = this, dfrd = $.Deferred(), cdn = './elfinder/ace/', init = function() { if (typeof ace === 'undefined') { console.log(cdn); this.fm.loadScript([ cdn + '/ace.js', cdn + '/ext-modelist.js', cdn + '/ext-settings_menu.js', cdn + '/ext-language_tools.js' ], start); } else { start(); }
},
start = function() { var editor, editorBase, mode, ta = $(textarea), taBase = ta.parent(), dialog = taBase.parent(), id = textarea.id + '_ace', ext = self.file.name.replace(/^.+\.([^.]+)|(.+)$/, '$1$2').toLowerCase(), // MIME/mode map mimeMode = { 'text/x-php': 'php', 'application/x-php': 'php', 'text/html': 'html', 'application/xhtml+xml': 'html', 'text/javascript': 'javascript', 'application/javascript': 'javascript', 'text/css': 'css', 'text/x-c': 'c_cpp', 'text/x-csrc': 'c_cpp', 'text/x-chdr': 'c_cpp', 'text/x-c++': 'c_cpp', 'text/x-c++src': 'c_cpp', 'text/x-c++hdr': 'c_cpp', 'text/x-shellscript': 'sh', 'application/x-csh': 'sh', 'text/x-python': 'python', 'text/x-java': 'java', 'text/x-java-source': 'java', 'text/x-ruby': 'ruby', 'text/x-perl': 'perl', 'application/x-perl': 'perl', 'text/x-sql': 'sql', 'text/xml': 'xml', 'application/docbook+xml': 'xml', 'application/xml': 'xml' };
// set basePath of ace ace.config.set('basePath', cdn);
// set base height taBase.height(taBase.height());
// detect mode mode = ace.require('ace/ext/modelist').getModeForPath('/' + self.file.name).name; if (mode === 'text') { if (mimeMode[self.file.mime]) { mode = mimeMode[self.file.mime]; } }
// show MIME:mode in title bar taBase.prev().children('.elfinder-dialog-title').append(' (' + self.file.mime + ' : ' + mode.split( /[\/\\]/).pop() + ')'); // TextArea button and Setting button $('<div class="ui-dialog-buttonset"/>').css('float', 'left') .append( $('<button>文本框</button>') .button() .on('click', function() { if (ta.data('ace')) { ta.removeData('ace'); editorBase.hide(); ta.val(editor.session.getValue()).show().focus(); $(this).text('编辑器'); } else { ta.data('ace', true); editorBase.show(); editor.setValue(ta.hide().val(), -1); editor.focus(); $(this).text('文本框'); } }) ) .append( $('<button>Ace editor setting</button>') .button({ icons: { primary: 'ui-icon-gear', secondary: 'ui-icon-triangle-1-e' }, text: false
}) .on('click', function() { editor.showSettingsMenu(); })
) .prependTo(taBase.next());
// Base node of Ace editor editorBase = $('<div id="' + id + '" style="width:100%; height:100%;"/>').text(ta.val()).insertBefore(ta .hide()); // Ace editor configure ta.data('ace', true); editor = ace.edit(id); ace.require('ace/ext/language_tools'); ace.require('ace/ext/settings_menu').init(editor); editor.$blockScrolling = Infinity; editor.setOptions({ theme: 'ace/theme/dawn', mode: 'ace/mode/' + mode, fontSize: '14px', wrap: true, enableBasicAutocompletion: true, enableSnippets: true, enableLiveAutocompletion: true }); editor.commands.addCommand({ name: "saveFile", bindKey: { win: 'Ctrl-s', mac: 'Command-s' }, exec: function(editor) { self.doSave(); } }); editor.commands.addCommand({ name: "closeEditor", bindKey: { win: 'Ctrl-w|Ctrl-q', mac: 'Command-w|Command-q' }, exec: function(editor) { self.doCancel(); } });
editor.resize(); dfrd.resolve(editor); }; // init & start init(); return dfrd; },
close: function(textarea, instance) { if (instance) { instance.destroy(); $(textarea).show(); }
}, save: function(textarea, instance) { instance && $(textarea).data('ace') && (textarea.value = instance.session.getValue()); },
focus: function(textarea, instance) { instance && $(textarea).data('ace') && instance.focus(); },
resize: function(textarea, instance, e, data) { instance && instance.resize(); } } ] },
quicklook: {
// to enable preview with Google Docs Viewer googleDocsMimes: ['application/pdf', 'image/tiff', 'application/vnd.ms-office', 'application/msword', 'application/vnd.ms-word', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ] } });</script>
小结
总体来说个人使用还是非常不错的,当然对于一些成熟的网盘系统还是有一些差距。
源码:
https://gitee.com/52itstyle/spring-boot-CloudDisk
在线演示地址:https://cloud.52itstyle.vip
作者:小柒 | 订阅号:爪哇笔记
推荐阅读