# 框架中关于文件的操作规范 2022.05.12 01:22 云原生服务,应该取消本地磁盘的概念。而文件系统中存在大量的文件操作,不可避免的处理资源的拉取, 存储等操作,因此我们设计了一套静态资源文件管理解决方案。 详细框架图见:文件权限架构3.png ## 网络硬盘 作为集群化,最简单的方案是在基础设施层解决文件存储问题这是一种非常简单的方式,我们购买统 一的网盘资源,比如云盘,NAS,OSS等服务。之后将这些服务挂在到K8S集群中, 成为K8S的网盘资源。 对于应用来说所有的操作相当于是对本地磁盘的操作。但是这样会带来更加复杂的网络架构。比如, 我们集群的一个节点,可以挂在200个POD(默认110个),一个集群通常由至少3个以上的节点组成, 那么网盘将被挂在非常多,而且同时多应用对文件系统读写,带来更多的问题。同时再次基础上, 我们不得不为每一个应用开发文件上传和下载接口。我们希望我们的集群应用专注与我们的业务本身, 而不是提供静态服务器能力,介绍业务后端流量压力,所有的静态服务,我们系统可以通过CDN等服务 提供网络加速访问能力。 ## FileAdatper&FileManager ### 第一阶段 FileManager初始设计,我们想通过基础层封装和重写FileInputStream和FileOutputStream,在应用中 提供统一的文件操作方式,而非对实际文件进行操作。将文件的操作对实际业务进行屏蔽,从而在不经过 修改业务代码即可完成本地环境和生成环境的文件系统的切换工作。 所以我们提供了以下接口: com.suisrc.kratos.file.FileManager ```java /** * 获取文件是否存在 */ boolean exists(String filename); /** * 获取文件输入流 */ InputStream getFileInputStream(String filename, boolean cache) throws IOException; /** * 默认不使用缓存 * 当文件很大时候,请使用缓存,以达到更好的运行效果 */ InputStream getFileInputStream(String filename) throws IOException; /** * 获取文件输出流 */ OutputStream getFileOutputStream(String filename, boolean cache) throws IOException; /** * 获取当前的输出流 * 文件很大时候,请使用缓存,以达到更好的运行效果 */ OutputStream getFileOutputStream(String filename) throws IOException; /** * 本地写出同步方式,但是OSS写出或者其他方式写出内容的时候,提出一种异步写出方案 * 使用时候,请自己把握好分寸, 注意,必须执行close方法,否者会导致写出线程死锁所以close操作应该在final中完成 */ OutputStream getAsyncOutputStream(String filename, Consumer callback) throws IOException; ``` ### 第二阶段 我们系统网络存储,比如AliyunOSS, LocalNas直接提供文件的下载,静态服务的文件下载不再经过我们的业务后台, 直连访问终端。这样解决的文件(特别是图片)下载导致耗尽网络宽带。 因此,我们再次增加了以下接口: ```java /** * 获取文件的下载地址 */ String getResourceIndex(String filename, long expireIn); /** * 获取服务器临时资源对应的正式内容 * 该接口,提供是应用通过index下载文件,而非前端访问文件能力 */ InputStream getResourceStreamByIndex(String index, Consumer filename) throws IOException; /** * 获取文件的下载地址 * 获取下载索引, 提供外网访问请求。可操作图片,例如打水印: * image/watermark,t_15,rotate_30,text_5byg5LiJMjHlubQ45pyINOaXpSAgICAgICAgICA=,fill_1 */ String getResourceIndexWithStyle(String filename, long expireIn, String style); ``` ### 第三阶段 我们提出新的需求,上载的静态资源,不希望通过我们的后端,而且由终端直接上载到文件系统中(AliyunOSS, LocalNas), 或者提供专用的文件管理服务,之后业务中不再开发文件上载,下载接口。 以此,我们再次增加了以下接口: ```java /** * 获取前端上传地址, 后端签名,前端上传 */ AsyncStream getUploadResourceIndex(String filename, long expireIn, long sizeRange); ``` 同时我们提供的专用的文件服务实例: dev: https://img.dev1.sims-cn.com uat: https://img.uat.sims-cn.com prod: https://img.con.plscn.com 令牌: Header: Authorization: Bearer {{KAT}} or Quary: sig={{SIG}} ```js // 文件下载, 得到文件下载地址 GET {{BASE}}/api/oss/v1/file?f=xx.png&style= Request: f: 资源路径 style: 资源加工方式 Response: { "success": true, "data": { "location": "https://f3.res.sims-cn.com/xxx" // 文件的下载地址 }, "traceId": "9ab91193e96b263eb34dc0f1527f1977" } // 重定向文件下载地址, 100x 缩略图 GET {{BASE}}/api/oss/v1/file/0?f=xx.png Request: f: 资源路径 Response: 302 -> f3.res.sims-cn.com/xxx 文件内容 // 重定向文件下载地址 GET {{BASE}}/api/oss/v1/file/1?f=xx.png&style= Request: f: 资源路径 style: 资源加工方式 Response: 302 -> f3.res.sims-cn.com/xxx 文件内容 // 获取文件上传地址 POST {{BASE}}/api/oss/v1/file/upload?folder=xxx&name=xx.png&permission=0377 Request: folder: 上传到的文件夹 name: 文件名名称 permission: 权限, 0377(8进制) -> 0377(0xff), 011(公有域), 111(租户域), 111(私有域); 1(删), 1(写), 1(读), 默认 001(私有域, 1,3) Response: { "success": true, "data": { "host": "https://f3.res.sims-cn.com/xx/?Signature=cfa335be24df50b1b93e0753023636ec", // 上传地址 "filename": "account/19123/F0bPz0GKEzt4857MLer32jEe4EkOQrRo.png", // 上传文件保存的最终地址 "params": null, // 上传的参数, aliyunoss, 该字段有值,需要放入mutlipart参数中 "headers": null // 上传的请求头 }, "traceId": "0ca6db0238c104f8fbcbcfbbf42b3c28" } // 直接上传文件 POST {{BASE}}/api/oss/v1/file/upload/1?folder=xxx&name=xx.png&permission=0377 -> file Request: folder: 上传到的文件夹 name: 文件名名称 permission: 权限, 0377(8进制) -> 0377(0xff), 011(公有域), 111(租户域), 111(私有域); 1(删), 1(写), 1(读), 默认 001(私有域, 1,3) file: 上传的文件 Response: { "success": true, "data": { "name": "account/19123/2jEe4EkOQrR.png", // 上传的文件最终保存的名字 "uri": "https://f3.res.sims-cn.com/xx/", // img server resource, 静态文件系统地址 "uri0": "https://f3.res.sims-cn.com/xx/" // lfs server resource, 最终文件系统地址 }, "traceId": "0ca6db0238c104f8fbcbcfbbf42b3c28" } ``` ### 第四阶段 再使用文件服务时候,文件的访问权限成为我们新的课题,对于一个平台,那些文件可以修改,那些文件可以删除,如果对平台的文件进行封禁访问, 这里就需要代理文件的权限问题,正如我们上面看到的,进行oss服务,我们将提供文件的0377权限,而这部分权限的设计,我们进入FileAdapter组件完成。 以下是FileAdapter提供的基础功能 com.myfmes.github.svc.file.FileAdapter ```java /** * 获取底层文件管理器 * @return */ FileManager getFileManager(); //=========================================================================================== /** * 获取文件文件下载地址, 通过file server,该方式,将文件权限交由文件服务器处理 * * 获取下载索引, 提供外网访问请求。可操作图片,例如打水印: * image/watermark,t_15,rotate_30,text_5byg5LiJMjHlubQ45pyINOaXpSAgICAgICAgICA=,fill_1 */ String getUriByFS(String filepath, String style); //=========================================================================================== /** * 获取文件的下载地址 * 使用离线模式,不会检查文件是否存在和权限 * 一般情况下,用户数据已经对用户可见,因此数据上附带的图片等资源信息是不需要鉴权的 */ String getResourceIndexOffline(String filepath, long expireIn); /** * 获取文件的下载地址 * 使用离线模式,不会检查文件是否存在和权限 * 一般情况下,用户数据已经对用户可见,因此数据上附带的图片等资源信息是不需要鉴权的 * * 获取下载索引, 提供外网访问请求。可操作图片,例如打水印: * image/watermark,t_15,rotate_30,text_5byg5LiJMjHlubQ45pyINOaXpSAgICAgICAgICA=,fill_1 */ String getResourceIndexWithStyleOffline(String filepath, long expireIn, String style); /** * 获取前端上传地址 * 如果filepath是新建的,且唯一的,那么可以认为filepath是一个新文件,不检查权限,需要业务保证是新建的文件 */ AsyncStream getUploadResourceIndexOffline(String filepath, int permission, long expireIn, long sizeRange); //=========================================================================================== // 内容中不提供InputStream和OutputStream的Offline接口, 一般认为,如果应用需要无权限操作文件,可以直接使用FileManager,而不是FileAdapter // FileAdapter是具有权限判定的文件操作接口适配器,而FileManager是底层文件操作适配器,提供LocalFileManager, NginxFileManger, AliyunFileManger等 // 需要注意,如果重写FileAdapterSercie时候,可能涉及[filepath -> index]转换问题 //=========================================================================================== /** * 获取文件的下载地址 */ String getResourceIndex(String filepath, long expireIn, boolean throwErr); /** * 获取文件的下载地址 * 获取下载索引, 提供外网访问请求。可操作图片,例如打水印: * image/watermark,t_15,rotate_30,text_5byg5LiJMjHlubQ45pyINOaXpSAgICAgICAgICA=,fill_1 */ String getResourceIndexWithStyle(String filepath, long expireIn, String style, boolean throwErr); /** * 获取前端上传地址, 本地模式不支持上传地址, Nginx和Aliyun支持 * permission 权限 <= 0377(0xff), 011(公有域), 111(租户域), 111(私有域); 1(删), 1(写), 1(读), 默认 001(私有域, 1,3) */ AsyncStream getUploadResourceIndex(String filepath, int permission, long expireIn, long sizeRange); //=========================================================================================== /** * 获取文件输入流, 带有权限判定,如果不需要权限,请使用用FileManager获取 * * @param filepath * @return * @throws IOException */ InputStream getFileInputStream(String filepath, boolean cache) throws IOException; /** * 默认不使用缓存, 带有权限判定,如果不需要权限,请使用FileManager获取 * 当文件很大时候,请使用缓存,以达到更好的运行效果 */ InputStream getFileInputStream(String filepath) throws IOException; /** * 获取文件输出流, 带有权限判定,如果不需要权限,请使用FileManager获取 * permission 权限 <= 0377(0xff), 111(公有域), 111(租户域), 111(私有域); 1(删), 1(写), 1(读), 默认 001(私有域, 1,3) */ OutputStream getFileOutputStream(String filepath, int permission, boolean cache) throws IOException; /** * 获取当前的输出流, 带有权限判定,如果不需要权限,请使用FileManager获取 * 文件很大时候,请使用缓存,以达到更好的运行效果 * permission 权限 <= 0377(0xff), 111(公有域), 111(租户域), 111(私有域); 1(删), 1(写), 1(读), 默认 001(私有域, 1,3) */ OutputStream getFileOutputStream(String filepath, int permission) throws IOException; /** * 本地写出同步方式,但是OSS写出或者其他方式写出内容的时候,提出一种异步写出方案 * 使用时候,请自己把握好分寸, 注意,必须执行close方法,否者会导致写出线程死锁所以close操作应该在final中完成 * 带有权限判定,如果不需要权限,请使用FileManager获取 * permission 权限 <= 0377(0xff), 111(公有域), 111(租户域), 111(私有域); 1(删), 1(写), 1(读), 默认 001(私有域, 1,3) */ OutputStream getAsyncOutputStream(String filepath, int permission, Consumer callback) throws IOException; //=============================================================================== /** * 获取文件是否存在 */ boolean exists(String filepath); //==================================================================================================== /** * 获取增加权限的文件名 * * 暂时不提供删除行为,因此标记1<<2暂时无效 * 可写(删)必定可读,可删不一定可写,需要注意, 公有域不存在公有域删除行为, 一般公有域都是用1,只读,请注意 * permission 权限 <= 0377(0xff), 111(公有域), 111(租户域), 111(私有域); 1(删), 1(写), 1(读), 默认 001(私有域, 1,3) */ FileInfo createPermission(String filepath, int permission); /** * 获取文件权限信息 */ FileInfo getFileInfo(String filepath); /** * 保存授权信息 */ boolean savePermission(FileInfo info); ``` 文件权限设计准则: 1.没有权限标记的文件,登录即可查看 2.图片强制标记PUBLIC, 即使不登录也可查看 3.剩下,需要判定登录,同时验证0377权限后才可查看 4.公有域可操作 5.租户域,租户ID相同,可操作 6.私有域,租户ID相同,且管理员,可操作 7.私有域,租户用户ID相同,可操作 8.私有域,用户ID相同,可操作 9.私有域,账号ID相同,可操作 10.特权域,可操作,特权域可以理解是我们平台管理人员集合 ### 第五阶段(规划中) 文件分享码,提供文件的共享,为私有域文件或者租户域文件提供区域内共享和授权访问能力。 ### 第六阶段(规划中) 前端资源OSS/CDN化,前端部署的js,css等静态资源迁移到静态服务器上,为终端用户提供更快的加载速度 ## 如何使用和配置 目前服务+组件配置说明 java 框架中组件支持 ```xml 1.3.3-SNAPSHOT com.myfmes.github.fwk openfile com.myfmes.github.fwk file com.myfmes.github.fwk file-api com.aliyun.oss aliyun-sdk-oss 3.14.0 ``` ```yml spring: three: file-manage0: client # 保障框架补缝切换,提供升级备用配置,优先加载 file-manager: aliyun # local nginx aliyun client, 目前支持4中FileManager local.file-mamager: index-url: http://xxx/{filename} # 本地文件,通过url地址请求的url地址 workspace: target # 本地缓存目录 client.file-manager: # 带有用户的访问令牌, 7:aliyun, 8: nginx config-uri: http://xxx/xxx/xxx refresh: 0 # 0: 不刷新, >0s 刷新 nginx.file-manager: endpoint: http://xxx endpoint-internal: http://xxx # 应用内网访问使用内网地址拉取数据,非必要配置 secret: bucket-folder: workspace: target # 本地缓存目录 aliyun.file-manager: aliyun-ecs: false # 优先使用内网 endpoint: http://xxx access-key-id: access-key-secret: bucket-name: bucket-folder: verify-oss: false # 验证服务器上是否存在文件,如果不存在,到本地检索后上传 cache-local: false # 上传文件,在本地留有备份 workspace: target # 本地缓存目录 file-adapter: file-server-sig: false file-server: http://xxx?f={filename}&style={style} domain-privilege: p6m # 默认值,不用配置,特权域标识 tenant-privilege: owner,admin # 默认值,不需要配置 ``` golang 框架中组件支持 ```go.mod github.com/suisrc/demo-gs v0.0.6 // package fmng import ( _ "github.com/suisrc/demo-gs/fmng/alioss" // aliyun _ "github.com/suisrc/demo-gs/fmng/clioss" // client _ "github.com/suisrc/demo-gs/fmng/lfsoss" // local _ "github.com/suisrc/demo-gs/fmng/ngxoss" // nginx ) ``` ```go GetUriByFS(user auth.User, filename string, style string) (string, error) GetResourceIndex(filename string, timeout time.Duration) (string, error) GetResourceIndexWithStyle(filename string, timeout time.Duration, style string) (string, error) GetUploadResourceIndex(filename string, timeout time.Duration, size int64) (*AsyncStream, error) ``` ```ini (toml) [fileconfig] kind="client" # local, client, nginx, aliyun workspace= fileserver="/api/oss/v1/file/1?f={filename}&style={style}" fileserversig=false domainprivilege="" tenantprivilege="" [clientoss] configuri="http://nfsc.res.local/oss/config/" refresh=0 [nginxoss] endpoint="https://f3.res.sims-cn.com" secret="" bucketfolder="" [aliyunoss] accesskeyid = "" accesskeysecret = "" endpoint = "https://oss-cn-shanghai.aliyuncs.com" bucketname = "" bucketfolder = "" ``` 在本地,我们提供类似aliyun oss相同功能的本地文件系统,具有阿里云类似的鉴权方式,实现技术为openresty + lua + gm(wand) + nas(raid01), FileManager实体为NginxFileManager, 在图片处理方面,提供resize和watermark两种方案,参数兼容aliyun oss方案。 process(style)=image/resize,w_100/watermark,t_15,rotate_30,text_5byg5LiJMjHlubQ45pyINOaXpSAgICAgICAgICA=,fill_1 ```s -- 图片水印 -- image/watermark,t_15,rotate_30,text_5byg5LiJMjHlubQ45pyINOaXpSAgICAgICAgICA=,fill_1 -- https://help.aliyun.com/document_detail/44957.html -- t: 指定图片水印或水印文字的透明度。 [0,100], 默认值: 100, 表示透明度100%(不透明)。 -- g: 指定水印在图片中的位置。 nw: 左上, north: 中上, ne: 右上,west: 左中, center: 中部, east: 右中, sw: 左下, south: 中下, se: 右下,默认值 -- x: 指定水印的水平边距, 即距离图片边缘的水平距离。这个参数只有当水印位置是左上、左中、左下、右上、右中、右下才有意义。 [0,4096] 默认值: 10 单位: 像素px -- y: 指定水印的垂直边距,即距离图片边缘的垂直距离, 这个参数只有当水印位置是左上、中上、右上、左下、中下、右下才有意义。 [0,4096] 默认值: 10 单位: px -- voffset: 指定水印的中线垂直偏移。当水印位置在左中、中部、右中时,可以指定水印位置根据中线往上或者往下偏移。 [-1000,1000] 默认值: 0 单位: px -- -- text: 指定文字水印的文字内容,文字内容需进行Base64编码。详情请参见水印编码。 Base64编码之前中文字符串的最大字节长度为64个字符。 -- type: 指定文字水印的字体,字体名称需进行Base64编码。 支持的字体及字体编码详情请参见文字类型编码对应表。 默认值: wqy-zenhei( 编码后的值为d3F5LXplbmhlaQ) -- color: 指定文字水印的文字颜色,参数值为RGB颜色值。 RGB颜色值,例如: 000000 表示黑色, FFFFFF 表示白色。 默认值: 000000 黑色 -- size: 指定文字水印的文字大小。(0,1000] 默认值: 40 单位: px -- shadow: 指定文字水印的阴影透明度。[0,100] 默认值: 0,表示没有阴影。 -- rotate: 指定文字顺时针旋转角度。[0,360] 默认值: 0,表示不旋转。 -- fill: 指定是否将文字水印铺满原图。1: 表示将文字水印铺满原图。0: 表示不将文字水印铺满全图,默认值 -- -- image: 用于指定作为图片水印Object的完整名称,Object名称需进行Base64编码。详情请参见水印编码。例如,作为图片水印的Object为Bucket内image目录下的panda.png,则需要编码的内容为image/panda.png,编码后的字符串为aW1hZ2UvcGFuZGEucG5n。 -- P: 指定图片水印按照原图的比例进行缩放,取值为缩放的百分比。如设置参数值为10,如果原图为100×100, 当原图变成了200×200,则图片水印大小为20×20。 -- T: 指定图片水印或水印文字的透明度。 [0,100], 默认值: 100, 表示透明度100%(不透明)。当文字和图片共存时候,图片专用参数 -- O: 指定图片载入方式,默认:Multiply(乘积), Dissolve(融合), Copy(覆盖)... -- 图片缩放 -- image/resize,w_500,h_500,m_fill -- m: 指定缩放的模式。 -- 1.lfit:默认值,等比缩放,缩放图限制为指定w与h的矩形内的最大图片。 -- 2.mfit:等比缩放,缩放图为延伸出指定w与h的矩形框外的最小图片。 -- 3.fill:将原图等比缩放为延伸出指定w与h的矩形框外的最小图片,之后将超出的部分进行居中裁剪。 -- 4.pad:将原图缩放为指定w与h的矩形内的最大图片,之后使用指定颜色居中填充空白部分。 -- 5.fixed:固定宽高,强制缩放。 -- w: 指定目标缩放图的宽度。 [1,4096] -- h: 指定目标缩放图的高度。 [1,4096] ``` ## 用例 获取一个文件的访问地址 https://img.uat.sims-cn.com/api/oss/v1/file/1?f=19122/aRA00000002QZX0GZ1noPfLS.png&style=image/quality,q_85/resize,h_100&sig=4ggzwhl03g0y.ZA.BDoCYdFSFN6wSZoArK8zH38It_HUBIM7QiiEEXctjCmb5rDWyX3j0DThHP5A22_BvA1Ht9RRz20.xko4Du9.ER ```java FileAdapter.getUriByFS("19122/aRA00000002QZX0GZ1noPfLS.png","image/quality,q_85/resize,h_100") ``` 该地址为静态资源服务地址,没有有效期,其中sig为访问令牌,跟当前登录令牌绑定,因此当用户登出,访问地址失效。 获取一个文件的访问地址 https://f3.res.sims-cn.com/19122/aRA00000002QZX0GZ1noPfLS.png?Expires=1652286216&Nonce=b05970bd8c8b&process=image/quality,q_85/resize,h_100&Signature=227409a31404c5adaa287f6479dfba18 ```java FileAdapter.getResourceIndex("19122/aRA00000002QZX0GZ1noPfLS.png", 60) FileAdapter.getResourceIndexWithStyle("19122/aRA00000002QZX0GZ1noPfLS.png", 60, "image/quality,q_85/resize,h_100") ``` 该地址为静态文件系统地址,具有有效期,其前面就是当前资源令牌,资源到期后自动失效。