包含的主要功能和示例
- 创建bucket
- 删除bucket
- 文件上传
- 拷贝文件
- 删除文件
- 文件下载
- 设置文件标签
- 上传文件指定时间自动删除
- 上传文件并加密
- 分片上传
- 断点续传
- 生成预签名url,直接前端上传不经过后端
源码地址
源码地址
使用demo地址
demo地址
前置测试环境
首先使用docker-compose安装了最新的minio用于测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| version: '3'
minio: image: minio/minio container_name: minio restart: always ports: - "9000:9000" - "9001:9001" environment: MINIO_ROOT_USER: "miniouser" MINIO_ROOT_PASSWORD: "123456789" volumes: - /mnt/minio/files:/data command: server /data --console-address ":9001"
|
项目引入依赖
这里说明一下cn.allbs这个group用于jdk1.8,com.alltobs用于jdk17+,但是cn.allbs中有一点点jdk17,遇到的话降级点版本。
1 2 3 4 5
| <dependency> <groupId>com.alltobs</groupId> <artifactId>alltobs-oss</artifactId> <version>1.0.0</version> </dependency>
|
项目配置
配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| oss: endpoint: http://xxx.xxx.xxx.xxx:9000 region: cn-north-1 access-key: adadmin secret-key: 123456778 bucket-name: test expiring-buckets: temp-bucket-1: 30 temp-bucket-2: 60
|
endpoint
就是安装minio或者腾讯云、阿里云之类的对象储存地址
region
所在地区
access-key
可以直接使用控制台账号,但是建议在控制台中生成账号和密钥
secret-key
可以直接使用控制台密钥,但是建议在控制台中生成账号和密钥
bucket-name
设置一个默认的文件桶,比如不同项目使用同一个文件库,以项目为文件桶分隔
expiring-buckets
里面设置的是包含生命周期的文件夹,创建位置位于bucket-name下,比如bucket-name叫test,那么会在test目录下创建一个生命周期为30天的文件夹temp-bucket-1,生命周期为60天的文件夹temp-bucket-2
启动类注解@EnableAllbsOss
使用引入
1 2
| @Resource private OssTemplate ossTemplate;
|
使用演示
以下所有方法都遵循一个原则,当bucket-name不为空时,所有操作都在这个bucket下进行。
自动创建主目录和带过期时间的子目录
就会自动创建文件桶如下,一个位于test
目录下且十天后会自动删除的expire-bucket-1
创建bucket
1 2 3 4 5
| @PostMapping("/createBucket") public ResponseEntity<String> createBucket(@RequestParam String bucketName) { ossTemplate.createBucket(bucketName); return ResponseEntity.ok("Bucket created: " + bucketName); }
|
查询所有bucket
1 2 3 4
| @GetMapping("/getAllBuckets") public R<List<String>> getAllBuckets() { return R.ok(ossTemplate.getAllBuckets()); }
|
删除指定bucket
1 2 3 4 5
| @DeleteMapping("/removeBucket") public R<String> removeBucket(@RequestParam String bucketName) { ossTemplate.removeBucket(bucketName); return R.ok("Bucket removed: " + bucketName); }
|
上传文件
1 2 3 4 5 6 7 8 9 10 11
| @PostMapping("/putObject") public R<String> putObject(@RequestParam String bucketName, @RequestParam MultipartFile file) { String fileName = file.getOriginalFilename(); try { String uuid = UUID.randomUUID() + "." + FileUtil.getFileType(fileName); ossTemplate.putObject(bucketName, uuid, file.getInputStream()); return R.ok("File uploaded: " + uuid); } catch (IOException e) { return R.fail("Failed to upload file: " + fileName); } }
|
查询指定目录下指定前缀的文件
1 2 3 4
| @GetMapping("/getAllObjectsByPrefix") public R<Set<String>> getAllObjectsByPrefix(@RequestParam String bucketName, @RequestParam String prefix) { return R.ok(ossTemplate.getAllObjectsByPrefix(bucketName, prefix).stream().map(S3Object::key).collect(Collectors.toSet())); }
|
查看文件(字节数组)
1 2 3 4 5 6 7 8
| @GetMapping("/getObject") public R<byte[]> getObject(@RequestParam String bucketName, @RequestParam String objectName) { try (var s3Object = ossTemplate.getObject(bucketName, objectName)) { return R.ok(s3Object.readAllBytes()); } catch (IOException e) { return R.fail(e.getLocalizedMessage()); } }
|
下载文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @GetMapping("/download") public void download(@RequestParam String bucketName, @RequestParam String objectName, HttpServletResponse response) { try (ResponseInputStream<GetObjectResponse> inputStream = ossTemplate.getObject(bucketName, objectName); OutputStream outputStream = response.getOutputStream()) { String contentType = inputStream.response().contentType(); response.setContentType(contentType); response.setHeader("Content-Disposition", "attachment; filename=\"" + objectName + "\""); inputStream.transferTo(outputStream); outputStream.flush(); } catch (IOException e) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } }
|
查看文件链接带过期时间
1 2 3 4
| @GetMapping("/getObjectURL") public R<String> getObjectURL(@RequestParam String bucketName, @RequestParam String objectName, @RequestParam int minutes) { return R.ok(ossTemplate.getObjectURL(bucketName, objectName, minutes)); }
|
删除文件
1 2 3 4 5
| @DeleteMapping("/removeObject") public R<String> removeObject(@RequestParam String bucketName, @RequestParam String objectName) { ossTemplate.removeObject(bucketName, objectName); return R.ok("Object removed: " + objectName); }
|
文件复制
1 2 3 4 5 6
| @PostMapping("/copyObject") public R<String> copyObject(@RequestParam String sourceBucketName, @RequestParam String sourceKey, @RequestParam String destinationBucketName, @RequestParam String destinationKey) { ossTemplate.copyObject(sourceBucketName, sourceKey, destinationBucketName, destinationKey); return R.ok("Object copied from " + sourceKey + " to " + destinationKey); }
|
查询指定文件的访问权限
1 2 3 4
| @GetMapping("/getObjectAcl") public R<String> getObjectAcl(@RequestParam String bucketName, @RequestParam String objectName) { return R.ok(ossTemplate.getObjectAcl(bucketName, objectName).toString()); }
|
设置指定文件的访问权限
1 2 3 4 5
| @PostMapping("/setObjectAcl") public R<String> setObjectAcl(@RequestParam String bucketName, @RequestParam String objectName, @RequestParam String acl) { ossTemplate.setObjectAcl(bucketName, objectName, acl); return R.ok("ACL set for object: " + objectName); }
|
启用或者关闭指定bucket版本控制
1 2 3 4 5
| @PostMapping("/setBucketVersioning") public R<String> setBucketVersioning(@RequestParam String bucketName, @RequestParam boolean enable) { ossTemplate.setBucketVersioning(bucketName, enable); return R.ok("Versioning set to " + (enable ? "Enabled" : "Suspended") + " for bucket: " + bucketName); }
|
给指定文件打标签
1 2 3 4 5
| @PostMapping("/setObjectTags") public R<String> setObjectTags(@RequestParam String bucketName, @RequestParam String objectName, @RequestBody Map<String, String> tags) { ossTemplate.setObjectTags(bucketName, objectName, tags); return R.ok("Tags set for object: " + objectName); }
|
获取指定文件的标签
1 2 3 4
| @GetMapping("/getObjectTags") public R<Map<String, String>> getObjectTags(@RequestParam String bucketName, @RequestParam String objectName) { return R.ok(ossTemplate.getObjectTags(bucketName, objectName)); }
|
上传一个会定时删除的文件
1 2 3 4 5 6 7 8 9 10 11
| @PostMapping("putObjectWithExpiration") public R<String> putObjectWithExpiration(@RequestParam String bucketName, @RequestParam MultipartFile file, @RequestParam int days) { String fileName = file.getOriginalFilename(); try { String uuid = UUID.randomUUID() + "." + FileUtil.getFileType(fileName); ossTemplate.putObjectWithExpiration(bucketName, uuid, file.getInputStream(), days); return R.ok("File uploaded: " + uuid); } catch (IOException e) { return R.fail("Failed to upload file: " + fileName); } }
|
上传加密文件
需要在服务端配置KMS,这里使用的是默认的AES256,其他可以调putObjectWithEncryption
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @PostMapping("/uploadWithEncryption") public R<String> uploadWithEncryption(@RequestParam String bucketName, @RequestParam("file") MultipartFile file) { String uuid = UUID.randomUUID() + "." + FileUtil.getFileType(file.getOriginalFilename()); try (InputStream inputStream = file.getInputStream()) { PutObjectResponse response = ossTemplate.uploadWithEncryption( bucketName, uuid, inputStream, file.getSize(), file.getContentType() ); return R.ok("File uploaded with encryption: " + response.toString()); } catch (IOException e) { return R.fail("File upload failed: " + e.getMessage()); } }
|
分片上传
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| @PostMapping("/uploadMultipart") public R<String> uploadMultipart(@RequestParam String bucketName, @RequestParam MultipartFile file) throws IOException, InterruptedException { String objectName = file.getOriginalFilename(); String uploadId = ossTemplate.initiateMultipartUpload(bucketName, objectName); long partSize = 5 * 1024 * 1024; long fileSize = file.getSize(); int partCount = (int) Math.ceil((double) fileSize / partSize); List<CompletedPart> completedParts = Collections.synchronizedList(new ArrayList<>()); ExecutorService executor = Executors.newFixedThreadPool(Math.min(partCount, 10)); for (int i = 0; i < partCount; i++) { final int partNumber = i + 1; long startPos = i * partSize; long size = Math.min(partSize, fileSize - startPos); executor.submit(() -> { try (InputStream inputStream = file.getInputStream()) { inputStream.skip(startPos); byte[] buffer = new byte[(int) size]; int bytesRead = inputStream.read(buffer, 0, (int) size); if (bytesRead > 0) { CompletedPart part = ossTemplate.uploadPart(bucketName, objectName, uploadId, partNumber, buffer); completedParts.add(part); } } catch (IOException e) { e.printStackTrace(); } }); } executor.shutdown(); executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); if (completedParts.size() == partCount) { completedParts.sort(Comparator.comparing(CompletedPart::partNumber)); ossTemplate.completeMultipartUpload(bucketName, objectName, uploadId, completedParts); return R.ok("Upload completed successfully uploadId: " + uploadId); } else { ossTemplate.abortMultipartUpload(bucketName, objectName, uploadId); return R.fail("Upload failed, some parts are missing."); } }
|
断点续传
uploadId
是上一步分片上传获取到的,可以做个的记录,方便断点续传时使用。我这边测试方法是分片上传过程中直接终止了服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| @PostMapping("/resumeMultipart") public R<String> resumeMultipart(@RequestParam String bucketName, @RequestParam MultipartFile file, @RequestParam String uploadId) throws IOException, InterruptedException { String objectName = file.getOriginalFilename(); byte[] fileBytes = file.getBytes(); List<CompletedPart> completedParts = ossTemplate.listParts(bucketName, objectName, uploadId); long partSize = 5 * 1024 * 1024; long fileSize = fileBytes.length; int partCount = (int) Math.ceil((double) fileSize / partSize); ExecutorService executor = Executors.newFixedThreadPool(Math.min(partCount, 10)); for (int i = completedParts.size(); i < partCount; i++) { final int partNumber = i + 1; long startPos = i * partSize; long size = Math.min(partSize, fileSize - startPos); executor.submit(() -> { try { byte[] buffer = Arrays.copyOfRange(fileBytes, (int) startPos, (int) (startPos + size)); CompletedPart part = ossTemplate.uploadPart(bucketName, objectName, uploadId, partNumber, buffer); synchronized (completedParts) { completedParts.add(part); } } catch (Exception e) { e.printStackTrace(); } }); } executor.shutdown(); executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); completedParts.sort(Comparator.comparing(CompletedPart::partNumber)); ossTemplate.completeMultipartUpload(bucketName, objectName, uploadId, completedParts); return R.ok("Upload resumed and completed successfully"); }
|
前端不通过后台服务器使用预签名的表单上传数据
使用这种方式可以让客户端能够直接与 S3 进行交互,减少了服务器的负担,并且可以利用 S3 的上传能力进行大文件的处理。
1 2 3 4 5 6 7
| @GetMapping("/generatePreSignedUrl") public R<String> generatePreSignedUrl(@RequestParam String bucketName, @RequestParam String objectName, @RequestParam int expiration) { String preSignedUrl = ossTemplate.generatePreSignedUrlForPut(bucketName, objectName, expiration); return R.ok(preSignedUrl); }
|
前面的demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>文件上传</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .upload-container { max-width: 500px; margin: 0 auto; padding: 20px; border: 2px solid #ccc; border-radius: 10px; text-align: center; } .file-input { margin-bottom: 20px; } .progress-bar { width: 100%; background-color: #f3f3f3; border-radius: 5px; overflow: hidden; margin-bottom: 10px; } .progress { height: 20px; background-color: #4caf50; width: 0; } </style> </head> <body> <div class="upload-container"> <h2>上传文件到 S3</h2> <input type="file" id="fileInput" class="file-input"/> <div class="progress-bar"> <div class="progress" id="progressBar"></div> </div> <button onclick="uploadFile()">上传文件</button> <p id="statusText"></p> </div> <script> async function uploadFile() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; if (!file) { alert('请选择一个文件进行上传'); return; } const result = await response.json(); if (result.code !== 200) { document.getElementById('statusText').innerText = '获取预签名URL失败'; return; } const presignedUrl = result.data; const xhr = new XMLHttpRequest(); xhr.open('PUT', presignedUrl, true); xhr.setRequestHeader('Content-Type', file.type); xhr.upload.onprogress = function(event) { if (event.lengthComputable) { const percentComplete = (event.loaded / event.total) * 100; document.getElementById('progressBar').style.width = percentComplete + '%'; } }; xhr.onload = function() { if (xhr.status === 200) { document.getElementById('statusText').innerText = '文件上传成功!'; } else { document.getElementById('statusText').innerText = '文件上传失败,请重试。'; } }; xhr.onerror = function() { document.getElementById('statusText').innerText = '文件上传过程中出现错误。'; }; xhr.send(file); } </script> </body> </html>
|