• Ceph相关


    Ceph基础知识和基础架构简介

    http://www.xuxiaopang.com/2020/10/09/list/#more大话Ceph

    http://www.xuxiaopang.com/2016/11/08/easy-ceph-CRUSH/ 大话Ceph CRUSH算法

    在Ceph中一切数据(图片、视频,音频、文本、字符串等)都看成个对象来存储(读取其二进制流存储),而不以其格式来区分它们。

    架构:

    RADOS:对象存储。底层存储系统,最常用的存储方式

    LIBRADOS:各种语言的客户端库

    RADOSGW、RBD、CEPH FS:基于RADOS和LIBRADOS进一步实现的 Web API库、块存储、文件系统

    可见,提供三种存储方式:对象存储、块存储、文件系统存储

    其他概念:

    pool(逻辑上,对象存储池)

    pg(逻辑上,placement group对象归置组)

    osd(物理上,object storage device可理解为一个硬盘,一台主机里可能有多个)

    pool、pg是逻辑上的概念,起到namespace的作用,以对数据进行分区,在创建一个pool时指定pg数。

     

    数据存储位置计算:

    存储一个对象时毋庸置疑需要指定对象名(如 objectKey),此外也指定了所属的pool,要做的就是确定对象最终应存在哪个OSD上。

    计算过程可由服务端完成,但是这样的话任何一个对象存储时都需要服务端计算存储位置,服务端压力会大。一个合格的分布式存储系统肯定应将此计算任务交由客户端来完成。

    计算过程主要包括两部分(PS:数据分布原理很简单,官方吹嘘时总爱故弄玄虚。万变不离其宗,数据分布原理与读硕时所做时空数据索引系统的本质上大同小异):

    1、由对象的pool和objectKey确定所属的pg,Ceph用hash实现(pg数改了后重hash的问题?由于创建一个pool时指定了pool数,相当于pg数固定所以不用考虑重hash?)

    2、确定一个pg映射到哪个或哪些osd上,对应到多个是为了冗余存储提高可靠性。Ceph采用CRUSH算法,本质上就是一种伪随机选择的过程:

    对于一个pg:

    a、CRUSH_HASH( PG_ID, OSD_ID, r ) ===> draw :其和每个osd分别确定一个随机数,称为draw

    b、( draw &0xffff ) * osd_weight ===> osd_straw :各osd的权重(该osd以T为单位的存储容量值)乘各自的draw得到一个值,称为straw

    c、pick up high_osd_straw :选straw最大的osd作为pg应存入的osd

    这里第一步中的 r 为一个常数,用于通过调节之来为同一个pg对应到多个osd上,如分别为0、1、2等。

    原理图如下:

     利用Ceph实现一个网盘:

      1 package com.marchon.sensestudy.web;
      2 
      3 import java.io.BufferedOutputStream;
      4 import java.io.IOException;
      5 import java.io.InputStream;
      6 import java.net.URL;
      7 import java.net.URLEncoder;
      8 import java.util.ArrayList;
      9 import java.util.Comparator;
     10 import java.util.Date;
     11 import java.util.HashMap;
     12 import java.util.HashSet;
     13 import java.util.List;
     14 import java.util.Map;
     15 import java.util.Set;
     16 import java.util.regex.Pattern;
     17 import java.util.stream.Collectors;
     18 import java.util.zip.ZipEntry;
     19 import java.util.zip.ZipOutputStream;
     20 
     21 import javax.servlet.http.HttpServletRequest;
     22 import javax.servlet.http.HttpServletResponse;
     23 
     24 import org.slf4j.Logger;
     25 import org.slf4j.LoggerFactory;
     26 import org.springframework.beans.factory.annotation.Autowired;
     27 import org.springframework.core.io.InputStreamResource;
     28 import org.springframework.http.HttpStatus;
     29 import org.springframework.http.MediaType;
     30 import org.springframework.http.ResponseEntity;
     31 import org.springframework.security.access.prepost.PreAuthorize;
     32 import org.springframework.web.bind.annotation.DeleteMapping;
     33 import org.springframework.web.bind.annotation.GetMapping;
     34 import org.springframework.web.bind.annotation.PostMapping;
     35 import org.springframework.web.bind.annotation.PutMapping;
     36 import org.springframework.web.bind.annotation.RequestBody;
     37 import org.springframework.web.bind.annotation.RequestParam;
     38 import org.springframework.web.bind.annotation.RestController;
     39 import org.springframework.web.multipart.MultipartFile;
     40 
     41 import com.amazonaws.services.s3.model.CannedAccessControlList;
     42 import com.amazonaws.services.s3.model.DeleteObjectsResult;
     43 import com.amazonaws.services.s3.model.ObjectListing;
     44 import com.amazonaws.services.s3.model.S3ObjectSummary;
     45 import com.marchon.sensestudy.common.config.ConfigParam;
     46 import com.marchon.sensestudy.common.utils.CephClientUtils;
     47 import com.marchon.sensestudy.common.utils.ControllerUtils;
     48 import com.marchon.sensestudy.responsewrapper.ApiCustomException;
     49 import com.marchon.sensestudy.responsewrapper.ApiErrorCode;
     50 
     51 /**
     52  * 用户存储空间管理(网盘功能)。存储位于Ceph上,Ceph本身是key-value存储,为模拟文件系统目录,这里key采用层级路径形式:${userId}+分隔符+${absolutePath}。<br>
     53  * 可在读或写时模拟文件系统层级关系,若 读维护则增删快查改慢、写维护则与之相反。这里在读时维护<br>
     54  * <br>
     55  * note:<br>
     56  * 1. 增、改需要校验文件(夹)名有效性,查、删不需要。文件或目录名不能包含/等;<br>
     57  * 2. 在Ceph上并没有目录概念,也不存储目录,所有数据都是以key-value存储的,这里key命名时采用层级路径形式如/a/b/1.jpg、/a/b/2、/a/b/3/,展现给用户的文件夹或文件列表通过解析层级路径来获取。<br>
     58  * 为区分Ceph中一个key代表的是用户的文件还是文件夹,此以最后一层级后是否带"/"来区分:带与不带分别表示用户的文件夹和文件。可见:<br>
     59  * <li><b>用户在"/a/b/c"下可看到文件夹"d"的充要条件是Ceph中存在以"/a/b/c/d/"为前缀的key(注意d后的"/"不可少)</b></li> <br>
     60  * <li>用户看到的文件夹"b"可能来源于Ceph中的两种形式:实际创建的文件夹(在层级末尾,即如key="/a/b/")和虚拟文件夹(在层级中间,如key="/a/b/c/"或"/a/b/c.jpg")。两者可能在Ceph中同时存在,如用户创建一个文件夹再往其中传文件时</li>
     61  * 3. 写(上传文件、创建文件夹、重命名)时要解决重名问题,确保存储的:同级"目录"下文件之间、文件夹之间、文件和文件夹之间都不重名。解决:加数字后缀(传文件或文件夹时)、抛错(重命名时)<br>
     62  * <br>
     63  * TODO 涉及到组合操作,如重命名时复制原数据到新数据然后删除原数据,如何确保原子性?
     64  */
     65 @RestController
     66 public class AccountStorageController {// 操作:查(文件信息、文件下载)、删、增(文件夹、文件)、改、存储空间大小(查、改)
     67     private static final Logger logger = LoggerFactory.getLogger(AccountStorageController.class);
     68     private static final String SEPERATOR_FOR_KEY = "/";
     69 
     70     @Autowired
     71     private ControllerUtils controllerUtils;
     72 
     73     // 1. 以下为网盘CRUD相关
     74     /** 查。模拟文件系统目录形式展现Ceph上的文件。可通过Ceph的带delimiter的listObjects获取到当前目录下文件和文件夹列表,但此时文件夹没有大小、修改时间等信息,故弃之自实现 */
     75     @GetMapping("/api/v1/account/storage/entries")
     76     @PreAuthorize("hasAnyRole( 'ROLE_STUDENT' , 'ROLE_TEACHER' )")
     77     public List<EntryItem> getFileOrFolderList(@RequestParam("parentDirAbsPath") String parentDirAbsPath,
     78             HttpServletRequest request) {
     79         parentDirAbsPath = parentDirAbsPath.endsWith(SEPERATOR_FOR_KEY) ? parentDirAbsPath
     80                 : (parentDirAbsPath + SEPERATOR_FOR_KEY);// 确保以分隔符结尾,否则查询结果会有false
     81         // positive
     82         String userId = controllerUtils.getUserIdFromJwt(request);
     83         String objKeyPrefix = getJoinedObjKey(userId, parentDirAbsPath);
     84 
     85         // 查询,获取key以objKeyPrefix为前缀的所有对象信息
     86         List<S3ObjectSummary> listRes = CephClientUtils
     87                 .listObjects(ConfigParam.cephBucketName_accountStorage, objKeyPrefix).stream()
     88                 .map(objList -> objList.getObjectSummaries()).flatMap(s3ObjList -> s3ObjList.stream())
     89                 .collect(Collectors.toList());
     90         // System.out.println(listRes);
     91 
     92         // 提取文件夹名或文件名
     93         Map<String, EntryItem> res = new HashMap<>();
     94         for (S3ObjectSummary s3ObjSum : listRes) {
     95             if (s3ObjSum.getKey().equals(objKeyPrefix)) {
     96                 continue;
     97             }
     98             // 获取纯文件名或目录名
     99             String entryName = s3ObjSum.getKey().substring(objKeyPrefix.length());// 去除前缀
    100             boolean isFolder = entryName.indexOf(SEPERATOR_FOR_KEY) >= 0;// 对于用户创建的文件夹Ceph存储key时以分隔符结尾
    101             entryName = isFolder ? entryName.substring(0, entryName.indexOf(SEPERATOR_FOR_KEY)) : entryName;// 得到目录名或文件夹名,不包含任何额外路径信息
    102 
    103             // 获取fileType:folder, file, txt, doc, ...
    104             int lastIndexOfDot = entryName.lastIndexOf(".");
    105             String fileType = isFolder ? SpecialFileType.FOLDER.typeName
    106                     : (lastIndexOfDot >= 0 && lastIndexOfDot < entryName.length() - 1
    107                             ? entryName.substring(lastIndexOfDot + 1)
    108                             : SpecialFileType.FILE.typeName);
    109             // 组织层级信息返回
    110             EntryItem entryItem = null;
    111             if (!res.containsKey(entryName)) {// 新建entry
    112                 entryItem = new EntryItem(parentDirAbsPath, entryName, s3ObjSum.getSize(), s3ObjSum.getLastModified(),
    113                         ConfigParam.cephBucketName_accountStorage + "/" + s3ObjSum.getKey(), fileType);
    114                 res.put(entryName, entryItem);
    115             } else {// 更新entry,只有文件夹会有此操作
    116                 entryItem = res.get(entryName);
    117                 // 更新数据大小
    118                 entryItem.setByteSize(entryItem.getByteSize() + s3ObjSum.getSize());
    119                 // 更新最后修改时间
    120                 if (s3ObjSum.getLastModified().after(entryItem.getLastModify())) {
    121                     entryItem.setLastModify(s3ObjSum.getLastModified());
    122                 }
    123             }
    124         }
    125         return res.values().stream().sorted(new Comparator<EntryItem>() {// 排序:文件夹靠前、再先按类型排、再按时间倒排
    126             private String fileType1, fileType2;
    127 
    128             @Override
    129             public int compare(EntryItem o1, EntryItem o2) {
    130                 fileType1 = o1.getFileType();
    131                 fileType2 = o2.getFileType();
    132 
    133                 // 文件夹靠前排
    134                 if (fileType1.equals(SpecialFileType.FOLDER.typeName)
    135                         && !fileType2.equals(SpecialFileType.FOLDER.typeName)) {
    136                     return -1;
    137                 } else if (!fileType1.equals(SpecialFileType.FOLDER.typeName)
    138                         && fileType2.equals(SpecialFileType.FOLDER.typeName)) {
    139                     return 1;
    140                 }
    141                 // 同为文件夹 或 同非文件夹:按类型排,类型相同再按时间倒排
    142                 if (fileType1.equals(fileType2)) {
    143                     return -(o1.getLastModify().compareTo(o2.getLastModify()));
    144                 }
    145                 return fileType1.compareTo(fileType2);
    146 
    147             }
    148         }).collect(Collectors.toList());
    149     }
    150 
    151     /** 查。下载文件或文件夹。zip压缩使用org.apache.tools.zip库而不是JDK自带zip库,后者继承自前者故常用API几乎一样。 */
    152     @GetMapping(value = "/api/v1/account/storage/entries/download")
    153     @PreAuthorize("hasAnyRole( 'ROLE_STUDENT' , 'ROLE_TEACHER' )")
    154     // @GetMapping(value = WebSecurityConfig.URL_TEST_PUBLIC)
    155     public ResponseEntity<InputStreamResource> downLoadFiles(@RequestParam("parentDirAbsPath") String parentDirAbsPath,
    156             @RequestParam("entryNames") List<String> entryNames, HttpServletRequest request,
    157             HttpServletResponse response) throws IOException {
    158         // 获取待下载entry的绝对路径
    159         parentDirAbsPath = parentDirAbsPath.endsWith(SEPERATOR_FOR_KEY) ? parentDirAbsPath
    160                 : parentDirAbsPath + SEPERATOR_FOR_KEY;
    161         List<String> entryAbsPathes = new ArrayList<>(entryNames.size());
    162         for (String entryName : entryNames) {
    163             entryAbsPathes.add(parentDirAbsPath + entryName);
    164         }
    165 
    166         String userId = controllerUtils.getUserIdFromJwt(request);
    167         // String userId = "63b3fb3e-a8a7-49bb-8d3b-93517955cf13";
    168         boolean isDownloadSingleFile = false;
    169 
    170         // 判断是否是单文件下载
    171         String objKey;
    172         if (entryAbsPathes.size() == 1) {
    173             // 检测是否是文件
    174             String entryAbsPath = entryAbsPathes.get(0);
    175             objKey = getJoinedObjKey(userId, entryAbsPath);
    176             if (!entryAbsPath.endsWith(SEPERATOR_FOR_KEY)
    177                     && CephClientUtils.isObjExist(ConfigParam.cephBucketName_accountStorage, objKey)) {// 文件存在
    178                 isDownloadSingleFile = true;
    179             }
    180         }
    181 
    182         // 下载
    183         if (isDownloadSingleFile) {// 单文件,直接下载
    184             objKey = getJoinedObjKey(userId, entryAbsPathes.get(0));
    185             if (CephClientUtils.isObjExist(ConfigParam.cephBucketName_accountStorage, objKey)) {// 文件存在
    186                 URL url = CephClientUtils.generatePresignedUrl(ConfigParam.cephBucketName_accountStorage, objKey, null);
    187                 // InputStream in = CephClientUtils.getObject(ConfigParam.cephBucketName_accountStorage, objKey)
    188                 // .getObjectContent();// doesn't work, can only get the first byte
    189 
    190                 String fileName = objKey.substring(objKey.lastIndexOf(SEPERATOR_FOR_KEY) + 1);
    191                 InputStream in = url.openStream();
    192                 // System.out.println("file " + objKey + " size: " + in.available());
    193                 return ResponseEntity.ok().contentLength(in.available()).contentType(MediaType.APPLICATION_OCTET_STREAM)
    194                         .header("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"))
    195                         .body(new InputStreamResource(in));
    196             }
    197         } else if (entryAbsPathes.size() > 0) {// 多文件下载,边压缩边下载
    198             // 获取指定路径在Ceph中的所有对象key
    199             Set<String> downloadObjKeys = new HashSet<>();
    200             for (String entryAbsPath : entryAbsPathes) {
    201                 if (entryAbsPath.trim().equals("")) {
    202                     continue;
    203                 }
    204                 objKey = getJoinedObjKey(userId, entryAbsPath);
    205                 if (!entryAbsPath.endsWith(SEPERATOR_FOR_KEY)
    206                         && CephClientUtils.isObjExist(ConfigParam.cephBucketName_accountStorage, objKey)) {// 文件存在
    207                     downloadObjKeys.add(objKey);
    208                 } else {// 当成下载文件夹
    209                     entryAbsPath = entryAbsPath.endsWith(SEPERATOR_FOR_KEY) ? entryAbsPath
    210                             : entryAbsPath + SEPERATOR_FOR_KEY;// 确保查询时key以分隔符结尾,防止false
    211                     // positive
    212                     objKey = getJoinedObjKey(userId, entryAbsPath);
    213                     // 列取该文件夹下的所有文件的key
    214                     List<String> tmpObjKeys = CephClientUtils
    215                             .listObjects(ConfigParam.cephBucketName_accountStorage, objKey).stream()
    216                             .map(objList -> objList.getObjectSummaries()).flatMap(s3ObjList -> s3ObjList.stream())
    217                             .map(e -> e.getKey())//
    218                             // .filter(key -> !key.endsWith(SEPERATOR_FOR_KEY))// 过滤掉空文件夹
    219                             .collect(Collectors.toList());
    220                     downloadObjKeys.addAll(tmpObjKeys);
    221                 }
    222             }
    223             // System.out.println(downloadObjKeys);
    224 
    225             // 下载
    226             response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
    227             response.setHeader("Content-Disposition", "attachment;filename="
    228                     + URLEncoder.encode(entryNames.size() == 1 ? entryNames.get(0) + ".zip" : "download.zip", "UTF-8"));// 若只有一个文件夹则压缩包直接以文件夹命名
    229 
    230             ZipOutputStream zipos = new ZipOutputStream(new BufferedOutputStream(response.getOutputStream())); // 设置压缩流:直接写入response,实现边压缩边下载
    231             zipos.setMethod(ZipOutputStream.DEFLATED); // 设置压缩方法
    232             // zipos.setEncoding("UTF-8");
    233 
    234             logger.info("user {} download {} files", userId, downloadObjKeys.size());
    235             byte[] buffer = new byte[20 * 1024];
    236             ZipEntry zipEntry;
    237             for (String downloadObjKey : downloadObjKeys) {
    238                 // 获取文件流
    239                 URL url = CephClientUtils.generatePresignedUrl(ConfigParam.cephBucketName_accountStorage,
    240                         downloadObjKey, null);
    241                 InputStream inputStream = url.openStream();
    242                 logger.info("file {} size: {}", downloadObjKey, inputStream.available());
    243 
    244                 // 添加ZipEntry
    245                 String fileName = downloadObjKey.substring(userId.length() + 1)// 去除userId前缀,包括分隔符
    246                         .substring(parentDirAbsPath.length() == 1 ? 0 : parentDirAbsPath.length());// 去除parentDirAbsPath前缀,parentDirAbsPath本身包括分隔符
    247                 zipEntry = new ZipEntry(fileName);
    248                 zipEntry.setComment("generated by marchon");
    249                 zipos.putNextEntry(zipEntry);// 名字带"/"后缀即可添加文件夹如/a/b/会添加文件夹b,/a/b/1.txt也会添加文件夹b。直接打开zip文件可能看不到空文件夹,但解压后即有
    250                 // 传数据
    251                 {
    252                     int length = 0;
    253                     while ((length = inputStream.read(buffer)) != -1) {
    254                         zipos.write(buffer, 0, length);
    255                     }
    256                 }
    257                 // 关闭流
    258                 inputStream.close();
    259                 zipos.closeEntry();
    260             }
    261             // 关闭流
    262             zipos.close();
    263         }
    264         return null;
    265     }
    266 
    267     /** 删。"*"表示删除当前所在目录下的所有内容 */
    268     @DeleteMapping("/api/v1/account/storage/entries")
    269     @PreAuthorize("hasAnyRole( 'ROLE_STUDENT' , 'ROLE_TEACHER' )")
    270     public List<String> deleteFiles(@RequestBody List<EntryItem> entryItems, HttpServletRequest request) {
    271         String userId = controllerUtils.getUserIdFromJwt(request);
    272 
    273         List<String> deletedEntryNameList = new ArrayList<>();
    274         for (EntryItem entryItem : entryItems) {
    275             String parentDirAbsPath = entryItem.getParentDirAbsPath();
    276             String entryName = entryItem.getEntryName();
    277             String fileType = entryItem.getFileType();// 用以确定是否是目录
    278 
    279             // 若entryItems元素非空,则每个元素的fileType、entryName必传
    280             ApiCustomException.assertTrue(HttpStatus.BAD_REQUEST, ApiErrorCode.NOT_FOUND_PARAM,
    281                     parentDirAbsPath != null,
    282                     "Bad Request: Required String parameter 'parentDirAbsPath' is not present");
    283             ApiCustomException.assertTrue(HttpStatus.BAD_REQUEST, ApiErrorCode.NOT_FOUND_PARAM, entryName != null,
    284                     "Bad Request: Required String parameter 'entryName' is not present");
    285             ApiCustomException.assertTrue(HttpStatus.BAD_REQUEST, ApiErrorCode.NOT_FOUND_PARAM, fileType != null,
    286                     "Bad Request: Required String parameter 'fileType' is not present");
    287 
    288             // 组合获取key
    289             parentDirAbsPath = parentDirAbsPath.endsWith(SEPERATOR_FOR_KEY) ? parentDirAbsPath
    290                     : (parentDirAbsPath + SEPERATOR_FOR_KEY);// 确保以分隔符结尾
    291 
    292             // 删除
    293             String objKeyPrefix;
    294             if (entryName.trim().equals("*") || fileType.equals(SpecialFileType.FOLDER.typeName)) {// 删除所有 或 删除文件夹
    295                 if (entryName.trim().equals("*")) {// 删除当前目录下的所有文件
    296                     objKeyPrefix = getJoinedObjKey(userId, parentDirAbsPath);
    297                 } else {
    298                     entryName = entryName.endsWith(SEPERATOR_FOR_KEY) ? entryName : entryName + SEPERATOR_FOR_KEY;// 若是文件夹则确保以分隔符结尾,避免误删。如若指定文件夹名为code,若不加/则codeExamples文件夹也会被删
    299                     objKeyPrefix = getJoinedObjKey(userId, parentDirAbsPath + entryName);
    300                 }
    301                 DeleteObjectsResult deletedRes = CephClientUtils
    302                         .deleteObjects(ConfigParam.cephBucketName_accountStorage, objKeyPrefix);
    303 
    304                 List<String> deletedObjPathes = deletedRes.getDeletedObjects().stream().map(ele -> ele.getKey())// 获取key
    305                         .map(key -> key.substring(userId.length(), key.length()))// 去掉userId前缀和/后缀
    306                         .collect(Collectors.toList());
    307                 deletedEntryNameList.addAll(deletedObjPathes);
    308             } else {// 删除文件
    309                 String objKey = getJoinedObjKey(userId, parentDirAbsPath + entryName);
    310                 if (CephClientUtils.isObjExist(ConfigParam.cephBucketName_accountStorage, objKey)) {
    311                     CephClientUtils.deleteObject(ConfigParam.cephBucketName_accountStorage, objKey);
    312                     deletedEntryNameList.add(objKey.substring(userId.length()));
    313                 }
    314             }
    315 
    316             // 解决删除后父目录丢失的问题
    317             {
    318                 // 删除完后对用户所见而言有可能丢了若干层父目录,如对于请求parentDirAbsPath=/a/b、fileType=folder,若b是虚拟文件夹且/a/b/下只有一个entry,则执行删除后,用户就看不到b文件夹了,若a是虚拟文件夹则a用户也看不到了,依之类推。
    319                 // 解决:创建一个parentDirAbsPath key
    320                 objKeyPrefix = getJoinedObjKey(userId, parentDirAbsPath);
    321                 int numOfKeyWithparentDirAbsPathPrefix = CephClientUtils
    322                         .listObjects(ConfigParam.cephBucketName_accountStorage, objKeyPrefix).stream()
    323                         .map(objList -> objList.getObjectSummaries()).flatMap(s3ObjList -> s3ObjList.stream())
    324                         .collect(Collectors.toList()).size();// 以parentDirAbsPath为前缀的对象数
    325                 // System.out.println(numOfKeyWithparentDirAbsPathPrefix);
    326                 if (numOfKeyWithparentDirAbsPathPrefix < 1) {
    327                     CephClientUtils.writeObject(ConfigParam.cephBucketName_accountStorage, objKeyPrefix, "");
    328                 }
    329             }
    330         }
    331 
    332         {// 更新capacity信息
    333             updateCurrentCapacity(userId);
    334         }
    335 
    336         return deletedEntryNameList;
    337     }
    338 
    339     /** 增。创建文件夹。须确保存到Ceph时文件夹名以 / 结尾;已存在该名字的文件或文件夹时加数字后缀 */
    340     @PostMapping(value = "/api/v1/account/storage/folder", produces = "application/json")
    341     @PreAuthorize("hasAnyRole( 'ROLE_STUDENT' , 'ROLE_TEACHER' )")
    342     public String createFolder(@RequestBody Map<String, Object> reqMap, HttpServletRequest request) {
    343         String parentDirAbsPath = (String) reqMap.get("parentDirAbsPath");
    344         String inputFolderName = (String) reqMap.get("folderName");
    345 
    346         ApiCustomException.assertTrue(HttpStatus.BAD_REQUEST, ApiErrorCode.NOT_FOUND_PARAM, parentDirAbsPath != null,
    347                 "Bad Request: Required String parameter 'parentDirAbsPath' is not present");
    348         ApiCustomException.assertTrue(HttpStatus.BAD_REQUEST, ApiErrorCode.NOT_FOUND_PARAM, inputFolderName != null,
    349                 "Bad Request: Required String parameter 'inputFolderName' is not present");
    350 
    351         ApiCustomException.assertTrue(ApiErrorCode.ILLEGAL_VALUE, isEntryNameValid(inputFolderName, true),
    352                 "Invalid folder name");
    353 
    354         String userId = controllerUtils.getUserIdFromJwt(request);
    355         parentDirAbsPath = parentDirAbsPath.endsWith(SEPERATOR_FOR_KEY) ? parentDirAbsPath
    356                 : (parentDirAbsPath + SEPERATOR_FOR_KEY);// 确保以分隔符结尾
    357         inputFolderName = inputFolderName.endsWith(SEPERATOR_FOR_KEY)
    358                 ? inputFolderName.substring(0, inputFolderName.length() - 1)
    359                 : inputFolderName;// 确保不易/结尾
    360 
    361         // 若当前目录下已存在同名文件夹或文件则加数字后缀
    362         String savedFolderName = getRenamedEntrynameIfExist(userId, parentDirAbsPath, inputFolderName, true);
    363 
    364         // 创建
    365         savedFolderName = savedFolderName + SEPERATOR_FOR_KEY;// 存入时确保以分隔符结尾
    366         String objKey = getJoinedObjKey(userId, parentDirAbsPath + savedFolderName);
    367         CephClientUtils.writeObject(ConfigParam.cephBucketName_accountStorage, objKey, "");
    368         logger.info("upload folder to ceph: " + objKey);
    369         return parentDirAbsPath + savedFolderName;
    370     }
    371 
    372     /** 增。上传文件。即使文件夹上传获取到的也只是其中的所有文件之。已存在该名字的文件或文件夹时加数字后缀 */
    373     @PostMapping(value = "/api/v1/account/storage/entries")
    374     @PreAuthorize("hasAnyRole( 'ROLE_STUDENT' , 'ROLE_TEACHER' )")
    375     public List<String> uploadFiles(@RequestParam("userFile") List<MultipartFile> files,
    376             @RequestParam("parentDirAbsPath") String parentDirAbsPath, HttpServletRequest request) throws Exception {
    377 
    378         parentDirAbsPath = parentDirAbsPath.endsWith(SEPERATOR_FOR_KEY) ? parentDirAbsPath
    379                 : (parentDirAbsPath + SEPERATOR_FOR_KEY);// 确保以分隔符结尾
    380         String userId = controllerUtils.getUserIdFromJwt(request);
    381         List<String> res = new ArrayList<>();
    382         for (MultipartFile file : files) {
    383             if (file.isEmpty()) {
    384                 continue;
    385             }
    386             ApiCustomException.assertTrue(ApiErrorCode.ILLEGAL_VALUE,
    387                     isEntryNameValid(file.getOriginalFilename(), false),
    388                     "Invalid file name '" + file.getOriginalFilename() + "'");// 不能包含/等
    389 
    390             {// TODO 文件过滤
    391                 // if (!file.getOriginalFilename().endsWith(".csv")) {// 类型过滤
    392                 // throw new ApiCustomException(ApiErrorCode.INVALID_FILE_CONTENT, "only support .csv file");
    393                 // }
    394                 // if (file.getSize() > 100) {// 大小过滤
    395                 // throw new ApiCustomException(ApiErrorCode.INVALID_FILE_CONTENT, "file to large");
    396                 // }
    397             }
    398 
    399             // 若当前目录下已存在同名文件夹或文件则加数字后缀
    400             String savedEntryName = getRenamedEntrynameIfExist(userId, parentDirAbsPath, file.getOriginalFilename(),
    401                     false);
    402 
    403             // 存储
    404             String objKey = getJoinedObjKey(userId, parentDirAbsPath + savedEntryName);
    405 
    406             {// TODO 修改权限,不能全public
    407                 CephClientUtils.writeObject(ConfigParam.cephBucketName_accountStorage, objKey, file,
    408                         CannedAccessControlList.PublicRead);
    409                 CephClientUtils.generatePublicUrl(ConfigParam.cephBucketName_accountStorage, objKey);
    410             }
    411             res.add(parentDirAbsPath + savedEntryName);
    412 
    413             logger.info("upload file to ceph:{}, size:{}", objKey, file.getSize());
    414 
    415         }
    416 
    417         {// 更新capacity信息
    418             updateCurrentCapacity(userId);
    419         }
    420         return res;
    421     }
    422 
    423     /** 改。新名字的文件夹或文件已存在时抛错 */
    424     @PutMapping("/api/v1/account/storage/entry")
    425     @PreAuthorize("hasAnyRole( 'ROLE_STUDENT' , 'ROLE_TEACHER' )")
    426     public List<EntryItem> updateEntryName(@RequestBody Map<String, Object> reqMap, HttpServletRequest request) {
    427         String parentDirAbsPath = (String) reqMap.get("parentDirAbsPath");
    428         String fileType = (String) reqMap.get("fileType");
    429         String oldEntryName = (String) reqMap.get("oldEntryName");
    430         String newEntryName = (String) reqMap.get("newEntryName");
    431 
    432         // 各参数必传
    433         ApiCustomException.assertTrue(HttpStatus.BAD_REQUEST, ApiErrorCode.NOT_FOUND_PARAM, parentDirAbsPath != null,
    434                 "Bad Request: Required String parameter 'parentDirAbsPath' is not present");
    435         ApiCustomException.assertTrue(HttpStatus.BAD_REQUEST, ApiErrorCode.NOT_FOUND_PARAM, fileType != null,
    436                 "Bad Request: Required String parameter 'fileType' is not present");
    437         ApiCustomException.assertTrue(HttpStatus.BAD_REQUEST, ApiErrorCode.NOT_FOUND_PARAM, oldEntryName != null,
    438                 "Bad Request: Required String parameter 'oldEntryName' is not present");
    439         ApiCustomException.assertTrue(HttpStatus.BAD_REQUEST, ApiErrorCode.NOT_FOUND_PARAM, newEntryName != null,
    440                 "Bad Request: Required String parameter 'newEntryName' is not present");
    441 
    442         // 确保新旧名不同
    443         if (oldEntryName.equals(newEntryName)) {
    444             return getFileOrFolderList(parentDirAbsPath, request);
    445         }
    446 
    447         String userId = controllerUtils.getUserIdFromJwt(request);
    448         parentDirAbsPath = parentDirAbsPath.endsWith(SEPERATOR_FOR_KEY) ? parentDirAbsPath
    449                 : (parentDirAbsPath + SEPERATOR_FOR_KEY);// 确保以分隔符结尾
    450         boolean isFolder = fileType.equals(SpecialFileType.FOLDER.typeName);
    451 
    452         ApiCustomException.assertTrue(ApiErrorCode.ILLEGAL_VALUE, isEntryNameValid(newEntryName, isFolder),
    453                 "Invalid file/folder name.");// 不能包含/等
    454 
    455         if (isFolder) {// 文件夹重命名
    456             oldEntryName = oldEntryName.endsWith(SEPERATOR_FOR_KEY) ? oldEntryName : oldEntryName + SEPERATOR_FOR_KEY;
    457             newEntryName = newEntryName.endsWith(SEPERATOR_FOR_KEY) ? newEntryName : newEntryName + SEPERATOR_FOR_KEY;
    458             String oldObjKeyPrefix = getJoinedObjKey(userId, parentDirAbsPath + oldEntryName);
    459             String newObjKeyPrefix = getJoinedObjKey(userId, parentDirAbsPath + newEntryName);
    460 
    461             // 确保新文件夹名的文件夹未存在
    462             if (CephClientUtils.listObjects(ConfigParam.cephBucketName_accountStorage, newObjKeyPrefix)
    463 
    464                     .stream().map(objList -> objList.getObjectSummaries()).flatMap(s3ObjList -> s3ObjList.stream())
    465                     .collect(Collectors.toList()).size() > 0) {
    466                 throw new ApiCustomException(ApiErrorCode.DUPLICATE_DATA, "the target folder already exists");
    467             }
    468             // 确保原文件夹存在
    469             List<S3ObjectSummary> listRes = CephClientUtils
    470                     .listObjects(ConfigParam.cephBucketName_accountStorage, oldObjKeyPrefix).stream()
    471                     .map(objList -> objList.getObjectSummaries()).flatMap(s3ObjList -> s3ObjList.stream())
    472                     .collect(Collectors.toList());
    473             if (listRes.size() < 1) {
    474                 throw new ApiCustomException(ApiErrorCode.NOT_EXIST_DATA, "the source folder doesn't exist");
    475             }
    476 
    477             // 移动
    478             String oldObjKey, newObjKey;
    479             for (S3ObjectSummary s3ObjSum : listRes) {
    480                 // 获取重命名后的新key,别直接replace因为有可能原key中有多处被replace掉
    481                 oldObjKey = s3ObjSum.getKey();
    482                 newObjKey = newObjKeyPrefix + oldObjKey.substring(oldObjKeyPrefix.length());// 去掉旧前缀,加上新前缀
    483                 // 转存
    484                 CephClientUtils.copyObject(ConfigParam.cephBucketName_accountStorage, oldObjKey,
    485                         ConfigParam.cephBucketName_accountStorage, newObjKey);
    486                 CephClientUtils.deleteObject(ConfigParam.cephBucketName_accountStorage, oldObjKey);
    487             }
    488         } else {// 文件重命名
    489             String oldObjKey = getJoinedObjKey(userId, parentDirAbsPath + oldEntryName);
    490             String newObjKey = getJoinedObjKey(userId, parentDirAbsPath + newEntryName);
    491 
    492             // 确保新文件名的文件未存在
    493             if (CephClientUtils.isObjExist(ConfigParam.cephBucketName_accountStorage, newObjKey)) {
    494                 throw new ApiCustomException(ApiErrorCode.DUPLICATE_DATA, "the target file already exists");
    495             }
    496             // 确保原文件存在
    497             if (!CephClientUtils.isObjExist(ConfigParam.cephBucketName_accountStorage, oldObjKey)) {
    498                 throw new ApiCustomException(ApiErrorCode.NOT_EXIST_DATA, "the source file doesn't exist");
    499             }
    500 
    501             CephClientUtils.copyObject(ConfigParam.cephBucketName_accountStorage, oldObjKey,
    502                     ConfigParam.cephBucketName_accountStorage, newObjKey);
    503             CephClientUtils.deleteObject(ConfigParam.cephBucketName_accountStorage, oldObjKey);
    504         }
    505         return getFileOrFolderList(parentDirAbsPath, request);
    506     }
    507 
    508     // 2. 以下为工具方法或工具类
    509     /** 检查当前目录下是否已经有与输入名同名的文件或文件夹,有则加数字后缀:若是文件夹或不带扩展名的文件则直接在尾部加后缀如 a -> a(1)、若是带扩展名的文件则在扩展名前加后缀如 a.jpg -> a(1).jpg */
    510     private String getRenamedEntrynameIfExist(String userId, String parentDirAbsPath, String inputEntryName,
    511             boolean isInputEntryFolder) {
    512 
    513         // 获取当前目录下同前缀的文件或文件夹key列表
    514         String objKey = getJoinedObjKey(userId, parentDirAbsPath + inputEntryName);
    515         List<ObjectListing> objectListings = CephClientUtils.listObjects(ConfigParam.cephBucketName_accountStorage,
    516                 objKey, SEPERATOR_FOR_KEY);
    517         List<String> fileKeysWithCommonprefix = objectListings.stream().map(e -> e.getObjectSummaries())
    518                 .flatMap(e -> e.stream()).map(e -> e.getKey()).collect(Collectors.toList());// 每个元素形如
    519                                                                                             // xx/a/b/1.jpg、xx/a/b/2
    520         List<String> folderKeysWithCommonprefix = objectListings.stream().map(e -> e.getCommonPrefixes())
    521                 .flatMap(e -> e.stream())// 每个元素形如xx/a/b/,末尾有"/"
    522                 .map(key -> key.substring(0, key.length() - 1)).collect(Collectors.toList());// 去除额外添加的"/"后缀以与filekey的形式一样
    523 
    524         // 查重
    525         Set<String> existedKeySet = new HashSet<>(fileKeysWithCommonprefix);
    526         existedKeySet.addAll(folderKeysWithCommonprefix);
    527         // System.out.println(existedKeySet);
    528         if (existedKeySet.contains(objKey)) {// 与同级目录下的现有文件或文件夹重名
    529             int lastIndexOfDot = inputEntryName.lastIndexOf(".");
    530             String prefix = inputEntryName;
    531             String suffix = "";
    532             // System.out.println(prefix + "," + suffix);
    533 
    534             // 带扩展名的文件由于在扩展名前加后缀,故还需选出去与除扩展名后的前缀同名的文件或文件夹。如已有文件a.jpg、文件夹a(1).jpg,此时再添加文件a.jpg时需把该文件夹也选出
    535             if (!isInputEntryFolder && 0 <= lastIndexOfDot && lastIndexOfDot < inputEntryName.length() - 1) {
    536                 prefix = inputEntryName.substring(0, lastIndexOfDot);
    537                 suffix = inputEntryName.substring(lastIndexOfDot);
    538                 String tmpObjKey = getJoinedObjKey(userId, parentDirAbsPath + prefix);
    539                 List<ObjectListing> tmpObjListings = CephClientUtils
    540                         .listObjects(ConfigParam.cephBucketName_accountStorage, tmpObjKey, SEPERATOR_FOR_KEY);
    541                 List<String> tmpFileKeysWithCommonprefix = tmpObjListings.stream().map(e -> e.getObjectSummaries())
    542                         .flatMap(e -> e.stream()).map(e -> e.getKey()).collect(Collectors.toList());
    543                 List<String> tmpFolderKeysWithCommonprefix = tmpObjListings.stream().map(e -> e.getCommonPrefixes())
    544                         .flatMap(e -> e.stream()).map(key -> key.substring(0, key.length() - 1))
    545                         .collect(Collectors.toList());
    546                 existedKeySet.addAll(tmpFileKeysWithCommonprefix);
    547                 existedKeySet.addAll(tmpFolderKeysWithCommonprefix);
    548                 // System.out.println(existedKeySet);
    549             }
    550             // 找出最小的未使用的数字后缀
    551             int size = existedKeySet.size();
    552             String tmpName;
    553             for (int i = 1; i <= size; i++) {
    554                 tmpName = prefix + "(" + i + ")" + suffix;
    555                 if (!existedKeySet.contains(getJoinedObjKey(userId, parentDirAbsPath + tmpName))) {
    556                     inputEntryName = tmpName;
    557                     break;
    558                 }
    559             }
    560         }
    561         return inputEntryName;
    562     }
    563 
    564     /** 以一个分隔符依次连接字段。需要确保输入的userId不以分隔符结尾 */
    565     private String getJoinedObjKey(String userId, String fileNameOrPath) {
    566         // 确保以一个 分隔符 连接各字段
    567         userId = userId.endsWith(SEPERATOR_FOR_KEY) ? userId.substring(0, userId.length() - 1) : userId;
    568         fileNameOrPath = fileNameOrPath.startsWith(SEPERATOR_FOR_KEY) ? fileNameOrPath
    569                 : SEPERATOR_FOR_KEY + fileNameOrPath;
    570         return userId + fileNameOrPath;
    571     }
    572 
    573     /** 增、改需要校验名字,查、删不需要 */
    574     private boolean isEntryNameValid(String entryName, boolean isFolder) {
    575         // String regEx = "[ _`~!@#$%^&*()+=|{}':;',\[\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]|
    |
    |	";
    576         String regEx = "[<>|*?,]";// <,>,|,*,?, 文件不能包含这些特殊字符,文件夹还不能包含分隔符等字符
    577 
    578         boolean res = entryName.length() > 0 && !Pattern.compile(regEx).matcher(entryName).find();
    579         if (isFolder) {
    580             res = res && !entryName.contains(SEPERATOR_FOR_KEY) && !entryName.equals(SpecialDirectory.CURRENT.name)
    581                     && !entryName.equals(SpecialDirectory.PARENT.name);// .和..分别表示当前目录和上层目录,系统默认,不能取该名。若前后或中间加空格则允许,不视为该二特殊目录
    582         }
    583         return res;
    584     }
    585 
    586     enum SpecialFileType {
    587         FOLDER("folder"), // 文件夹类型
    588         FILE("file");// 未知具体格式者指定为此类型
    589         public String typeName;
    590 
    591         private SpecialFileType(String typeName) {
    592             this.typeName = typeName;
    593         }
    594     }
    595 
    596     enum SpecialDirectory {
    597         CURRENT("."), // 当前目录
    598         PARENT("..");// 父目录
    599         String name;
    600 
    601         private SpecialDirectory(String name) {
    602             this.name = name;
    603         }
    604     }
    605 
    606     // 3. 以下为存储容量相关。容量信息存到Ceph,需要确保不与上传文件或文件夹的key重名。key:userId, value:
    607     // ${currentCapacity}_${totalCapacity}。
    608     /** 获取容量,优先从capacityInfo取currentCapacity而不是通过listObjects算得,以减少开销。 */
    609     @GetMapping("/api/v1/account/storage/capacity")
    610     @PreAuthorize("hasAnyRole( 'ROLE_STUDENT' , 'ROLE_TEACHER' )")
    611     public Map<String, Long> getCapacityInfo(HttpServletRequest request) {
    612 
    613         String userId = controllerUtils.getUserIdFromJwt(request);
    614         List<Long> capacityList = getCapacity(userId);
    615 
    616         // 组装结果
    617         Map<String, Long> res = new HashMap<>();
    618         res.put("currentCapacityByte", capacityList.get(0));
    619         res.put("totalCapacityByte", capacityList.get(1));
    620         return res;
    621     }
    622 
    623     /** 扩容 */
    624     @PutMapping("/api/v1/account/storage/capacity")
    625     @PreAuthorize("hasAnyRole( 'ROLE_STUDENT' , 'ROLE_TEACHER' )")
    626     public Map<String, Long> updateTotalCapacity(@RequestParam("totalCapacityGB") Double newTotalCapacityGB,
    627             HttpServletRequest request) {
    628 
    629         String userId = controllerUtils.getUserIdFromJwt(request);
    630         List<Long> capacityList = getCapacity(userId);
    631 
    632         long currentCapacity = capacityList.get(0);
    633 
    634         long newTotalCapacityBytes = (long) (newTotalCapacityGB * 1024 * 1024 * 1024);
    635         ApiCustomException.assertTrue(ApiErrorCode.ILLEGAL_VALUE, newTotalCapacityBytes >= currentCapacity,
    636                 "the totalCapacity is less than currentCapacity");
    637 
    638         // 更新
    639         String objKey = userId;
    640         String objValue = currentCapacity + "_" + newTotalCapacityBytes;
    641         CephClientUtils.writeObject(ConfigParam.cephBucketName_accountStorage, objKey, objValue);
    642 
    643         // 组装结果
    644         Map<String, Long> res = new HashMap<>();
    645         res.put("currentCapacityByte", currentCapacity);
    646         res.put("totalCapacityByte", newTotalCapacityBytes);
    647         return res;
    648     }
    649 
    650     /** 通过listObjects更新存储的currentCapacity信息,以供查询使用。应该在增或删操作之后调用此 */
    651     private long updateCurrentCapacity(String userId) {
    652         long currentCapacity, totalCapacity;
    653 
    654         String objKey = userId;
    655         if (CephClientUtils.isObjExist(ConfigParam.cephBucketName_accountStorage, objKey)) {
    656             String tmpStr = CephClientUtils.getObjectStr(ConfigParam.cephBucketName_accountStorage, objKey);
    657             String[] capacityStrs = tmpStr.split("_");
    658             totalCapacity = Long.parseLong(capacityStrs[1]);
    659         } else {
    660             totalCapacity = (long) (ConfigParam.cephAccountStorageDefaultCapacityGB * 1024 * 1024 * 1024);
    661         }
    662 
    663         currentCapacity = getCurrentCapacityByListingObjects(userId);
    664 
    665         String objValue = currentCapacity + "_" + totalCapacity;
    666         CephClientUtils.writeObject(ConfigParam.cephBucketName_accountStorage, objKey, objValue);
    667 
    668         return currentCapacity;
    669     }
    670 
    671     /** 获取Capacity信息,返回两个值:依次为currentCapacity、totalCapacity */
    672     private List<Long> getCapacity(String userId) {
    673         long currentCapacity, totalCapacity;
    674 
    675         String objKey = userId;
    676         if (CephClientUtils.isObjExist(ConfigParam.cephBucketName_accountStorage, objKey)) {
    677             String tmpStr = CephClientUtils.getObjectStr(ConfigParam.cephBucketName_accountStorage, objKey);
    678             String[] capacityStrs = tmpStr.split("_");
    679             currentCapacity = Long.parseLong(capacityStrs[0]);
    680             totalCapacity = Long.parseLong(capacityStrs[1]);
    681         } else {
    682             currentCapacity = getCurrentCapacityByListingObjects(userId);
    683             totalCapacity = (long) (ConfigParam.cephAccountStorageDefaultCapacityGB * 1024 * 1024 * 1024);
    684         }
    685 
    686         List<Long> res = new ArrayList<>();
    687         res.add(currentCapacity);
    688         res.add(totalCapacity);
    689         return res;
    690     }
    691 
    692     /** 获取指定用户的实际已用存储空间,单位byte。通过listObjects实现,比较耗时耗资源,尽量少调用 */
    693     private long getCurrentCapacityByListingObjects(String userId) {
    694         long currentCapacity = 0;
    695 
    696         String objKeyPrefix = userId.endsWith(SEPERATOR_FOR_KEY) ? userId : userId + SEPERATOR_FOR_KEY;// 确保以分隔符结尾,以免存储capacity的object也被包含在内
    697         List<S3ObjectSummary> s3ObjectSummaryList = CephClientUtils
    698                 .listObjects(ConfigParam.cephBucketName_accountStorage, objKeyPrefix).stream()
    699                 .map(e -> e.getObjectSummaries()).flatMap(e -> e.stream()).collect(Collectors.toList());
    700         for (S3ObjectSummary s3ObjSum : s3ObjectSummaryList) {
    701             currentCapacity += s3ObjSum.getSize();
    702         }
    703         return currentCapacity;
    704     }
    705 }
    706 
    707 /** 文件或文件夹抽象表示为Entry */
    708 class EntryItem {
    709     private String parentDirAbsPath;
    710     private String entryName;
    711     private Long byteSize;
    712     private Date lastModify;
    713     private String accessPath;
    714     private String fileType;// pdf, xls, txt ...
    715 
    716     public EntryItem() {// necessary
    717 
    718     }
    719 
    720     public EntryItem(String parentDirAbsPath, String entryName, Long byteSize, Date lastModify, String accessPath,
    721             String fileType) {
    722         this.parentDirAbsPath = parentDirAbsPath;
    723         this.entryName = entryName;
    724         this.byteSize = byteSize;
    725         this.lastModify = lastModify;
    726         this.accessPath = accessPath;
    727         this.fileType = fileType;
    728     }
    729 
    730     public String getParentDirAbsPath() {
    731         return parentDirAbsPath;
    732     }
    733 
    734     public void setParentDirAbsPath(String parentDirAbsPath) {
    735         this.parentDirAbsPath = parentDirAbsPath;
    736     }
    737 
    738     public String getEntryName() {
    739         return entryName;
    740     }
    741 
    742     public void setEntryName(String entryName) {
    743         this.entryName = entryName;
    744     }
    745 
    746     public Long getByteSize() {
    747         return byteSize;
    748     }
    749 
    750     public void setByteSize(Long byteSize) {
    751         this.byteSize = byteSize;
    752     }
    753 
    754     public Date getLastModify() {
    755         return lastModify;
    756     }
    757 
    758     public void setLastModify(Date lastModify) {
    759         this.lastModify = lastModify;
    760     }
    761 
    762     public String getAccessPath() {
    763         return accessPath;
    764     }
    765 
    766     public void setAccessPath(String accessPath) {
    767         this.accessPath = accessPath;
    768     }
    769 
    770     public String getFileType() {
    771         return fileType;
    772     }
    773 
    774     public void setFileType(String fileType) {
    775         this.fileType = fileType;
    776     }
    777 }
    CRUD API
      1 package com.marchon.sensestudy.common.utils;
      2 
      3 import java.io.BufferedReader;
      4 import java.io.IOException;
      5 import java.io.InputStream;
      6 import java.io.InputStreamReader;
      7 import java.io.UnsupportedEncodingException;
      8 import java.net.URL;
      9 import java.util.ArrayList;
     10 import java.util.Date;
     11 import java.util.List;
     12 import java.util.stream.Collectors;
     13 
     14 import org.springframework.web.multipart.MultipartFile;
     15 
     16 import com.amazonaws.ClientConfiguration;
     17 import com.amazonaws.Protocol;
     18 import com.amazonaws.auth.AWSCredentials;
     19 import com.amazonaws.auth.BasicAWSCredentials;
     20 import com.amazonaws.services.s3.AmazonS3;
     21 import com.amazonaws.services.s3.AmazonS3Client;
     22 import com.amazonaws.services.s3.model.CannedAccessControlList;
     23 import com.amazonaws.services.s3.model.CopyObjectResult;
     24 import com.amazonaws.services.s3.model.DeleteObjectsRequest;
     25 import com.amazonaws.services.s3.model.DeleteObjectsRequest.KeyVersion;
     26 import com.amazonaws.services.s3.model.DeleteObjectsResult;
     27 import com.amazonaws.services.s3.model.ListObjectsRequest;
     28 import com.amazonaws.services.s3.model.ObjectListing;
     29 import com.amazonaws.services.s3.model.ObjectMetadata;
     30 import com.amazonaws.services.s3.model.PutObjectRequest;
     31 import com.amazonaws.services.s3.model.PutObjectResult;
     32 import com.amazonaws.services.s3.model.S3Object;
     33 import com.marchon.sensestudy.common.config.ConfigParam;
     34 
     35 public class CephClientUtils {
     36 
     37     private static AmazonS3 amazonS3Client = null;
     38     private static Protocol protocol = Protocol.HTTP;
     39     public static String CEPH_URL_STR = protocol.name() + "://" + ConfigParam.cephHost + ":" + ConfigParam.cephPort;
     40 
     41     static {
     42         AWSCredentials credentials = new BasicAWSCredentials(ConfigParam.cephAccessKey, ConfigParam.cephSecretKey);
     43         ClientConfiguration clientConfig = new ClientConfiguration();
     44         clientConfig.setProtocol(protocol);
     45         amazonS3Client = new AmazonS3Client(credentials, clientConfig);
     46         amazonS3Client.setEndpoint(ConfigParam.cephHost + ":" + ConfigParam.cephPort);
     47     }
     48 
     49     /** 批量删除key以指定前缀开头的数据 */
     50     public static DeleteObjectsResult deleteObjects(String bucketName, String keyPrefix) {
     51         List<String> keyStrs = amazonS3Client.listObjects(bucketName, keyPrefix).getObjectSummaries().stream()
     52                 .map(e -> e.getKey()).collect(Collectors.toList());
     53         List<KeyVersion> keys = keyStrs.stream().map(e -> new KeyVersion(e)).collect(Collectors.toList());
     54         DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(bucketName);
     55         deleteObjectsRequest.setKeys(keys);
     56         return amazonS3Client.deleteObjects(deleteObjectsRequest);
     57     }
     58 
     59     /** 删除精确指定key的数据 */
     60     public static void deleteObject(String bucketName, String objKey) {
     61         amazonS3Client.deleteObject(bucketName, objKey);
     62     }
     63 
     64     /**
     65      * 获取存储的资源信息列表。通过正确指定prefix和delimiter可以以类文件系统的方式列取资源信息,即若多个文件在当前目录的同一个子文件夹下则它们只以一个子文件夹的形合并显示在当前目录下。这里的合并规则为
     66      * ”${prefix}w*${first_delimiter}“,即 prefix到delimiter首次出现
     67      * 间(包括prefix和delimiter自身)的subKey视为共有文件夹。缺点:这里的”文件夹“缺乏最近修改时间、大小等元信息。<br>
     68      * 典型应用:<br>
     69      * <li>若delimiter为空串则查得所有以prefix为前缀的key;</li><br>
     70      * <li>若所存的key是以"/"分隔的如"xx/a/b"、"xx/a/b"等,则当delimiter为"/"是:若prefix以"/"结尾则查得的是当前目录下的所有文件夹或文件、否则查得的是当前目录下以prefix为前缀的所有文件夹或文件</li>
     71      * 
     72      * @return 返回的ObjectListing List至少有一个元素
     73      */
     74     public static List<ObjectListing> listObjects(String bucketName, String keyPrefix, String delimiter) {
     75         ListObjectsRequest listObjectsRequest = new ListObjectsRequest();
     76         listObjectsRequest.setBucketName(bucketName);
     77         listObjectsRequest.setPrefix(keyPrefix);
     78         listObjectsRequest.setDelimiter(delimiter);
     79         // listObjectsRequest.setMaxKeys(2);// 1000 in default
     80         // listObjectsRequest.setMarker(null);
     81 
     82         // 可能有多个truncate
     83         List<ObjectListing> objectListings = new ArrayList<>();
     84         ObjectListing res = amazonS3Client.listObjects(listObjectsRequest);
     85         objectListings.add(res);
     86         while (res.isTruncated()) {
     87             res = amazonS3Client.listNextBatchOfObjects(res);
     88             objectListings.add(res);
     89         }
     90         return objectListings;
     91     }
     92 
     93     public static List<ObjectListing> listObjects(String bucketName, String keyPrefix) {
     94         return listObjects(bucketName, keyPrefix, null);
     95     }
     96 
     97     /** 文件存储 */
     98     public static PutObjectResult writeObject(String bucketName, String objKey, MultipartFile file,
     99             CannedAccessControlList cannedAccessControlList) {
    100         PutObjectResult res;
    101         if (!amazonS3Client.doesBucketExist(bucketName)) {
    102             amazonS3Client.createBucket(bucketName);
    103         }
    104         ObjectMetadata objectMetadata = new ObjectMetadata();
    105         objectMetadata.setContentLength(file.getSize()); // must set, otherwise stream contents will be buffered in
    106                                                             // memory and could result in out of memory errors.
    107         // objectMetadata.getUserMetadata().put("type", "pdf");//are added in http request header, which cann't only
    108         // contain iso8859-1 charset
    109         InputStream inputStream = null;
    110         try {
    111             inputStream = file.getInputStream();
    112         } catch (IOException e1) {
    113             e1.printStackTrace();
    114         }
    115         res = amazonS3Client.putObject(new PutObjectRequest(bucketName, objKey, inputStream, objectMetadata));
    116         if (null != cannedAccessControlList) {
    117             amazonS3Client.setObjectAcl(bucketName, objKey, cannedAccessControlList);
    118         }
    119         try {
    120             inputStream.close();
    121         } catch (IOException e) {
    122             e.printStackTrace();
    123         }
    124         return res;
    125     }
    126 
    127     /** 文件获取 */
    128     public static S3Object getObject(String bucketName, String objKey) {
    129         return amazonS3Client.getObject(bucketName, objKey);
    130     }
    131 
    132     public static PutObjectResult writeObject(String bucketName, String objKey, MultipartFile file) {
    133         return writeObject(bucketName, objKey, file, null);
    134     }
    135 
    136     /** 生成直接访问的永久URL。要求被访问资源的权限为public才能访问到 */
    137     public static URL generatePublicUrl(String bucketName, String objKey) {
    138         return amazonS3Client.getUrl(bucketName, objKey);
    139     }
    140 
    141     /** 生成直接访问的临时URL,失效时间默认15min */
    142     // url formate returned: scheme://host[:port]/bucketName/objKey?{Query}
    143     public static URL generatePresignedUrl(String bucketName, String key, Date expiration) {
    144         return amazonS3Client.generatePresignedUrl(bucketName, key, expiration);// 失效时间点必设,默认为15min,最多只能7天
    145     }
    146 
    147     /**
    148      * 5 default metadata are set, for example: <br>
    149      * {Accept-Ranges=bytes, Content-Length=5, Content-Type=text/plain, ETag=5d41402abc4b2a76b9719d911017c592,
    150      * Last-Modified=Sun Nov 04 15:35:17 CST 2018}
    151      */
    152     public static ObjectMetadata getObjectMetaData(String bucketName, String objKey) {
    153         return amazonS3Client.getObjectMetadata(bucketName, objKey);
    154     }
    155 
    156     public static boolean isObjExist(String bucketName, String objKey) {
    157         return amazonS3Client.doesObjectExist(bucketName, objKey);
    158     }
    159 
    160     public static CopyObjectResult copyObject(String sourceBucketName, String sourceKey, String destinationBucketName,
    161             String destinationKey) {
    162         return amazonS3Client.copyObject(sourceBucketName, sourceKey, destinationBucketName, destinationKey);
    163     }
    164 
    165     /** 文本存储,会被s3 sdk以UTF-8格式编码成字节流存储 */
    166     public static PutObjectResult writeObject(String bucketName, String objKey, String objContent) {
    167         if (null == objContent) {// objContent为null时下面putObject会出错
    168             objContent = "";
    169         }
    170         if (!amazonS3Client.doesBucketExist(bucketName)) {
    171             amazonS3Client.createBucket(bucketName);
    172         }
    173         return amazonS3Client.putObject(bucketName, objKey, objContent);// String will be encoded to bytes with UTF-8
    174                                                                         // encoding.
    175     }
    176 
    177     /** 文本获取 */
    178     public static String getObjectStr(String bucketName, String objKey) {
    179         if (!amazonS3Client.doesObjectExist(bucketName, objKey)) {// 判断是否存在,不存在时下面直接获取会报错
    180             return null;
    181         }
    182 
    183         S3Object s3Object = amazonS3Client.getObject(bucketName, objKey);
    184         BufferedReader bufferedReader = null;
    185         try {
    186             bufferedReader = new BufferedReader(new InputStreamReader(s3Object.getObjectContent(), "UTF-8"));// 存入时被UTF-8编码了,故对应之
    187         } catch (UnsupportedEncodingException e1) {
    188             e1.printStackTrace();
    189         }
    190 
    191         String res = null;
    192         try {
    193             res = bufferedReader.readLine();
    194         } catch (IOException e) {
    195             e.printStackTrace();
    196         }
    197         try {
    198             bufferedReader.close();
    199         } catch (IOException e) {
    200             e.printStackTrace();
    201         }
    202 
    203         return res;
    204     }
    205 
    206     private static byte[] getObjectBytes(String bucketName, String objKey) throws IOException {
    207         S3Object s3Object = amazonS3Client.getObject(bucketName, objKey);
    208         InputStream in = s3Object.getObjectContent();
    209 
    210         byte[] bytes = new byte[(int) s3Object.getObjectMetadata().getInstanceLength()];
    211         in.read(bytes);
    212 
    213         in.close();
    214         return bytes;
    215     }
    216 }
    CephClientUtils
  • 相关阅读:
    思路
    结合BeautifulSoup和hackhttp的爬虫实例
    hackhttp模板的介绍
    beauifulsoup模块的介绍
    php api_token 与 user_token 简析
    打造属于自己的火狐插件浏览器
    提高记忆力的习惯
    浏览器允许的并发请求资源数是什么意思?
    awk 进阶,百万行文件取交集
    ubuntu-docker入门到放弃(七)Dockerfile简介
  • 原文地址:https://www.cnblogs.com/z-sm/p/9531263.html
Copyright © 2020-2023  润新知