视频资源迁移到OSS服务器上,记录一下迁移过程。

搭建流程

在阿里云上购买oss,并获取具有该Bucket访问权限的AccessKey ID和AccessKey Secret信息。

数据迁移方案一

第一次尝试,不修改后端代码,直接将oss直接挂载映射到本地服务器。发现这种方法并不起作用,由于oss最终传输资源都要经由服务器再下发给用户,传输速度依然受限于服务器带宽,并没有起到加速的作用。

操作流程如下:

(1) 下载安装包。

以下载CentOS 7.0 (x64)版本为例:

sudo wget http://gosspublic.alicdn.com/ossfs/ossfs_1.80.6_centos7.0_x86_64.rpm

(2) 安装ossfs。

(3) CentOS系统/Anolis系统

以CentOS 7.0(x64)版本为例,安装命令如下:

sudo yum install ossfs_1.80.6_centos7.0_x86_64.rpm

(4) 配置账号访问信息。

将Bucket名称以及具有该Bucket访问权限的AccessKey ID和AccessKey Secret信息存放在/etc/passwd-ossfs文件中。文件的权限建议设置为640。

sudo **echo** BucketName:yourAccessKeyId:yourAccessKeySecret > /etc/passwd-ossfs

sudo **chmod** 640 /etc/passwd-ossfs

BucketName、yourAccessKeyId、yourAccessKeySecret请按需替换为您实际的Bucket名称、AccessKey ID和AccessKey Secret,例如:

sudo **echo** bucket-test:LTAIbZcdmQ****:MOk8x0y1coh7A5e2MZEUz**** > /etc/passwd-ossfs

sudo **chmod** 640 /etc/passwd-ossfs

(5)将Bucket挂载到指定目录。

由于我们需要将视频文件进行挂载,此时的挂载目录是/data/docker/videoSystem_v,且BucketName是video-bucket01。

示例如下:

**mkdir** /tmp/ossfs

ossfs video-bucket01 /data/docker/videoSystem_v/videoRes -o url=http://oss-cn-hangzhou.aliyuncs.com

(6)由于系统是跑在docker里面,数据是存储到容器里,因此需要把容器里的数据与oss映射的路径做绑定,这里通过创建docker数据卷做绑定:

 docker volume create --driver local \

  --opt type=none \

  --opt device=/data/docker/videoSystem_v/videoRes \

  --opt o=bind \

  oss_volume_name

(7)启动docker对应镜像,并作为数据卷的绑定:

docker run -p 8181:8181 -v oss_volume_name:/videoSystem_workdir/videoRes --name videoSystem --restart unless-stopped -d videosystem:v4.0

(8)Bucket卸载指令

sudo fusermount -u /data/docker/videoSystem_v/videoRes

​ 测试结果:60mb的视频上传,需要花费6分钟才能上传结束。如果不采用OSS映射方案,一分钟就能结束,上传反而更花费时间,这里推测是因为上传需要先上传到服务器本地内存,然后再从服务器本地上传到OSS服务器,并且中间存在映射关系,服务器自身有其他的业务压力,使得耗费时间更长。并且,上传的1080p视频,从用户网站打开播放,不能流畅播放,跑的依然是原先的服务器带宽,如果直接使用OSS上的视频url链接,绕过服务器,则能直接流畅播放1080p的视频。

​ 总结:该方案并不能实现预期的效果,因此下一方案将从后端代码层面进行修改。

数据迁移方案二

方案二从代码端进行更改,并且更改上传的模式——由前端进行上传。

配置OSS

1、本套方案采用前端上传文件的方式,需要前端向请求sts权限认证,为实现这套流程,需要在OSS控制台申请RAM用户,并且配置相关的请求sts认证权限。具体流程参考:

使用STS临时访问凭证访问OSS (aliyun.com)

2、保存RAM用户的访问密钥(AccessKey ID 和 AccessKey Secret)。

3、将bucket的权限设置为公共读,其文件的url是永久有效的,方便我们存到数据库以及后续通过url进行文件的下载和播放。

后端代码修改

新增AliyunConfig类:

@Component
@ConfigurationProperties(prefix = "aliyun")
@Data
public class AliyunConfig { // aliyun endpoint
private String endpoint; // aliyun accessKeyId
private String accessKeyId; // aliyun accessKeySecret
private String accessKeySecret; // aliyun bucketName
private String bucketName; // aliyun urlPrefix
private String urlPrefix; // aliyun stsEndPoint
private String stsEndPoint; // 用户权限角色
private String roleArn; // OSS所在区域
private String region; // OSS回调地址
private String callBackUrl; // 3D存储路径
private String StorageDirXA; // XR存储路径
private String StorageDirXB; @Bean
public OSS oSSClient() {
return new OSSClient(endpoint, accessKeyId, accessKeySecret);
}
}

在applicantion.yml新增配置信息:

aliyun:
endpoint: oss-cn-shenzhen.aliyuncs.com
accessKeyId: LTAI5t**********B9vBbD
accessKeySecret: 0FI3UB*****3KxnZdKNpHKWcY
bucketName: ******
urlPrefix: https:********.com
stsEndPoint: sts.cn-shenzhen.aliyuncs.com
roleArn: acs:ram::10*****2863:role/ramosstest
region: oss-cn-shenzhen
callBackUrl: https://**********
StorageDir3d: videoRes/shareXA
StorageDirXr: videoRes/shareXB

回调上传

@ResponseBody
@GetMapping("/getSts")
public AjaxResult getSts(HttpServletRequest req) throws com.aliyuncs.exceptions.ClientException {
AssumeRoleResponse.Credentials credentials = null;
String roleSessionName = "AliyunDMSRol--ePolicy";
// OSS服务器上设置的角色拥有所有权限,可以在下面policy添加需要的权限,按需要给予相关权限
String policy = "{\n" +
" \"Statement\": [\n" +
" {\n" +
" \"Action\": [\n" +
" \"oss:GetObject\",\n" +
" \"oss:PutObject\",\n" +
// 前端暂时不需要删除权限
//" \"oss:DeleteObject\",\n" +
" \"oss:ListParts\",\n" +
" \"oss:AbortMultipartUpload\",\n" +
" \"oss:ListObjects\"\n" +
// 下面的oss:* 拥有所有权限 慎用
//" \"oss:*\"\n" +
" ],\n" +
" \"Effect\": \"Allow\",\n" +
" \"Resource\": [\n" +
" \"acs:oss:*:*:colorlight-video/*\",\n" +
" \"acs:oss:*:*:colorlight-video\"\n" +
" ]\n" +
" }\n" +
" ],\n" +
" \"Version\": \"1\"\n" +
"}";
// 设置临时访问凭证的有效时间为3600秒。
Long durationSeconds = 3600L;
Map<String, Object> map = new HashMap<>();
try {
// 添加endpoint(直接使用STS endpoint,前两个参数留空,无需添加region ID)
DefaultProfile.addEndpoint("", "", "Sts", aliyunConfig.getStsEndPoint());
// 构造default profile(参数留空,无需添加region ID)
IClientProfile profile = DefaultProfile.getProfile("", aliyunConfig.getAccessKeyId(), aliyunConfig.getAccessKeySecret());
// 用profile构造client
DefaultAcsClient client = new DefaultAcsClient(profile);
final AssumeRoleRequest request = new AssumeRoleRequest();
request.setMethod(MethodType.POST);
request.setRoleArn(aliyunConfig.getRoleArn());
request.setRoleSessionName(roleSessionName);
request.setPolicy(policy); // Optional
request.setProtocol(ProtocolType.HTTPS); // 必须使用HTTPS协议访问STS服务);
final AssumeRoleResponse response = client.getAcsResponse(request);
credentials = response.getCredentials();
map = ObjectToMapUtil.objectToMap(credentials);
map.put("bucket", aliyunConfig.getBucketName());
map.put("region",aliyunConfig.getRegion());
CallBack callback = new CallBack();
callback.setUrl(aliyunConfig.getCallBackUrl() + "/clt3D/videoUpload/callBack");
callback.setBody("{fileName:${object},size:${size},mimeType:${mimeType},videoName:${x:videoName},definition:${x:definition}}");
callback.setContentType("application/json");
map.put("callback", callback);
} catch (Exception e) {
e.printStackTrace();
}
return AjaxResult.success(map);
}

上传成功后OSS调用回调接口,返回上传的文件的数据:

 @ResponseBody
@PostMapping("/callBack")
@Log(title = "新增视频", module = "clt3D", businessType = BusinessTypeEnum.INSERT)
public AjaxResult callBack(HttpServletRequest request, HttpServletResponse response) {
Video3D newVideo3D;
String fileUUID;
try {
AliyunCallbackUtil aliyunCallbackUtil = new AliyunCallbackUtil();
String ossCallBackBody = aliyunCallbackUtil.GetPostBody(request.getInputStream(), Integer.parseInt(request.getHeader("content-length")));
JSONObject jsonBody = JSON.parseObject(ossCallBackBody);
String fileName = jsonBody.getString("fileName");
String videoOriginalFilename = jsonBody.getString("videoName");
String videoName = NameUtil.format3DVideoName(ResourceUtil.getFileNamePrefix(videoOriginalFilename));
String definition = jsonBody.getString("definition");
String size = jsonBody.getString("size");
String ossUrl = aliyunConfig.getUrlPrefix();
String fileDir;
boolean zipFlag = false;
boolean verifyResult = aliyunCallbackUtil.VerifyOSSCallbackRequest(request, ossCallBackBody);
// 回调签名验证是不是OSS发来的签名
if (!verifyResult) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return AjaxResult.failed("OSS回调签名验证失败");
}
String[] videoNameArray = videoOriginalFilename.split("\\.");
//获取文件类型 video 或者是 image
String fileType = videoNameArray[videoNameArray.length - 1];
//拿到文件名
String[] fileNameArray = fileName.split("/");
String realName = fileNameArray[3];
String[] realNameArray = realName.split("_");
fileUUID = realNameArray[0];
// 判断上传的是不是压缩视频
if (realNameArray[1].equals("zip")) {
Video3D video3D = video3DService.getOne(new QueryWrapper<Video3D>().eq("mark", fileUUID));
video3D.setVideoCompressionUrl(ossUrl + "/" + fileName);
boolean updateFlag = video3DService.updateById(video3D);
if (!updateFlag) {
return AjaxResult.failed("压缩文件上传失败");
} else {
return AjaxResult.success("上传成功");
}
}else {
newVideo3D = new Video3D().setMark(fileUUID)
.setType(0)
.setVideoName(videoName)
.setUploadTime(new Date());
String fileSize = FileUtil.getFileSizeDescription(Integer.parseInt(size));
switch (definition) {
case "360p": // 判断属于哪种清晰度,决定保存的文件名
fileDir = "video360p";
newVideo3D.setVideo360pFormat(fileType);
newVideo3D.setVideo360pSize(fileSize);
newVideo3D.setVideo360pUrl(ossUrl + "/" + fileName);
break;
case "480p":
fileDir = "video480p";
newVideo3D.setVideo480pFormat(fileType);
newVideo3D.setVideo480pSize(fileSize);
newVideo3D.setVideo480pUrl(ossUrl + "/" + fileName);
break;
case "720p":
fileDir = "video720p";
newVideo3D.setVideo720pFormat(fileType);
newVideo3D.setVideo720pSize(fileSize);
newVideo3D.setVideo720pUrl(ossUrl + "/" + fileName);
break;
case "1080p":
fileDir = "video1080p";
newVideo3D.setVideo1080pFormat(fileType);
newVideo3D.setVideo1080pSize(fileSize);
newVideo3D.setVideo1080pUrl(ossUrl + "/" + fileName);
break;
default:
break;
}
}
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.failed("上传失败");
} // 从微信小程序服务器获取视频对应的QR码
try {
newVideo3D.setQrUrl(getQr(getUsableToken().getAccessToken(), fileUUID));
} catch (Exception e) {
e.printStackTrace();
} //把上传的文件数据入库
try {
video3DService.save(newVideo3D);
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.failed("上传失败");
}
return AjaxResult.success("上传成功");
}

回调中用到的AliyunCallbackUtil ,使用到了OSS调用回调接口传来的公钥和签名,采用RSA加密方式进行认证

public class AliyunCallbackUtil extends HttpServlet {

   private static final long serialVersionUID = 5522372203700422672L;

   /********************************
* @method : executeGet
* @function : 生成公钥
* @parameter : [url]
* @return : java.lang.String
* @date : 2023/7/21 14:24
********************************/
public String executeGet(String url) {
BufferedReader in = null;
String content = null;
try {
// 定义HttpClient
@SuppressWarnings("resource")
DefaultHttpClient client = new DefaultHttpClient();
// 实例化HTTP方法
HttpGet request = new HttpGet();
request.setURI(new URI(url));
HttpResponse response = client.execute(request);
in = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
StringBuffer sb = new StringBuffer("");
String line = "";
String NL = System.getProperty("line.separator");
while ((line = in.readLine()) != null) {
sb.append(line + NL);
}
in.close();
content = sb.toString();
} catch (Exception e) {
} finally {
if (in != null) {
try {
in.close();// 最后要关闭BufferedReader
} catch (Exception e) {
e.printStackTrace();
}
}
return content;
}
} /********************************
* @method : GetPostBody
* @function : 获取回调Body内容
* @parameter : [is, contentLen]
* @return : java.lang.String
* @date : 2023/7/21 14:24
********************************/
public String GetPostBody(InputStream is, int contentLen) {
if (contentLen > 0) {
int readLen = 0;
int readLengthThisTime = 0;
byte[] message = new byte[contentLen];
try {
while (readLen != contentLen) {
readLengthThisTime = is.read(message, readLen, contentLen - readLen);
if (readLengthThisTime == -1) {// Should not happen.
break;
}
readLen += readLengthThisTime;
}
return new String(message);
} catch (IOException e) {
}
}
return "";
} /********************************
* @method : VerifyOSSCallbackRequest
* @function : 验证回调请求
* @parameter : [request, ossCallbackBody]
* @return : boolean
* @date : 2023/7/21 14:24
********************************/
public boolean VerifyOSSCallbackRequest(HttpServletRequest request, String ossCallbackBody) throws NumberFormatException, IOException
{
// 整个验证流程是获取autorization 然后生成公钥,用公钥验签 使用请求url+body 与原先autorization作匹配 相同则验签就成功
boolean ret = false;
String autorizationInput = new String(request.getHeader("Authorization"));
String pubKeyInput = request.getHeader("x-oss-pub-key-url");
byte[] authorization = BinaryUtil.fromBase64String(autorizationInput);
byte[] pubKey = BinaryUtil.fromBase64String(pubKeyInput);
String pubKeyAddr = new String(pubKey);
if (!pubKeyAddr.startsWith("http://gosspublic.alicdn.com/") && !pubKeyAddr.startsWith("https://gosspublic.alicdn.com/"))
{
System.out.println("pub key addr must be oss addrss");
return false;
}
// 生成公钥
String retString = executeGet(pubKeyAddr);
retString = retString.replace("-----BEGIN PUBLIC KEY-----", "");
retString = retString.replace("-----END PUBLIC KEY-----", "");
String queryString = request.getQueryString();
String uri = request.getRequestURI();
String decodeUri = java.net.URLDecoder.decode(uri, "UTF-8");
String authStr = decodeUri;
if (queryString != null && !queryString.equals("")) {
authStr += "?" + queryString;
}
authStr += "\n" + ossCallbackBody;
ret = doCheck(authStr, authorization, retString);
return ret;
} /********************************
* @method : doCheck
* @function : 检查回调签名是否正确
* @parameter : [content, sign, publicKey]
* @return : boolean
* @date : 2023/7/21 14:25
********************************/
public static boolean doCheck(String content, byte[] sign, String publicKey) {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] encodedKey = BinaryUtil.fromBase64String(publicKey);
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
java.security.Signature signature = java.security.Signature.getInstance("MD5withRSA");
signature.initVerify(pubKey);
signature.update(content.getBytes());
boolean bverify = signature.verify(sign);
return bverify; } catch (Exception e) {
e.printStackTrace();
}
return false;
}

涉及到文件由后端进行上传的代码部分:

File file = new File(filepath);
FileOutputStream fos = new FileOutputStream(file);
byte[] b = new byte[1024];
while ((is.read(b)) != -1) {
fos.write(b);// 写入数据
}
is.close();
fos.close();// 保存数据
//添加写入oss //添加写入oss
try {
ossClient.putObject(aliyunConfig.getBucketName(), "videoRes/share3D/" + mark + "/" + fileName, file);
} catch (Exception e) {
e.printStackTrace();
}
//把本地的FILE文件删除
String qrDir = ResourceUtil.getStorageParentXADir() + mark + "/";
File qrFile = new File(qrDir);
FileUtil.delBatchFile(qrFile);
return aliyunConfig.getUrlPrefix() + "/videoRes/shareXA/" + mark + "/" + fileName;
}

涉及文件从后端向OSS获取的代码部分:

File file = new File(fileDir);
String objectName = "videoRes/shareXA/"
+ mark + "/"
+ mark
+ SystemConstant.QR_FILE_NAME_SUFFIX_XA
+ SystemConstant.QR_IMAGE_FORMAT_DEFAULT_PNG;
try {
// 下载Object到本地文件,并保存到指定的本地路径中。如果指定的本地文件存在会覆盖,不存在则新建。
// 先创建出文件
if (!file.exists()) {
//先得到文件的上级目录,并创建上级目录,在创建文件
file.getParentFile().mkdir();
try {
//创建文件
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
ossClient.getObject(new GetObjectRequest(aliyunConfig.getBucketName(), objectName), file);
} catch (Exception e) {
e.printStackTrace();
}
FileUtil.fileToZip(fileDir, zipOut, newVideoName);
File imageTargetFile = new File(ResourceUtil.getStorageParent3DDir()
+ mark + "/");
FileUtil.delBatchFile(imageTargetFile);
}

涉及文件删除的代码:

public AjaxResult deleteVideos(@RequestBody JSONObject reqObj) {
JSONArray marksArray = reqObj.getJSONArray("marks");
if (marksArray.size() == 0) {
return AjaxResult.failed(ResultCodeEnum.PARAM_FORMAT_ERROR);
}
int count = 0;
for (int i = 0; i < marksArray.size(); i++) {
String mark = marksArray.getString(i);
try {
if (video3DService.remove(new QueryWrapper<Video3D>().eq("mark", mark))) {
count++;
}
favoritesVideo3DService.remove(new QueryWrapper<FavoritesVideo3D>().eq("mark", mark));
String prefix = "videoRes/share3D/" + mark + "/";
// 列举所有包含指定前缀的文件并删除。
String nextMarker = null;
ObjectListing objectListing = null;
do {
ListObjectsRequest listObjectsRequest = new ListObjectsRequest(aliyunConfig.getBucketName())
.withPrefix(prefix)
.withMarker(nextMarker);
objectListing = ossClient.listObjects(listObjectsRequest);
if (objectListing.getObjectSummaries().size() > 0) {
List<String> keys = new ArrayList<String>();
for (OSSObjectSummary s : objectListing.getObjectSummaries()) {
System.out.println("key name: " + s.getKey());
keys.add(s.getKey());
}
DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(aliyunConfig.getBucketName()).withKeys(keys).withEncodingType("url");
DeleteObjectsResult deleteObjectsResult = ossClient.deleteObjects(deleteObjectsRequest);
List<String> deletedObjects = deleteObjectsResult.getDeletedObjects();
for (String obj : deletedObjects) {
String deleteObj = URLDecoder.decode(obj, "UTF-8");
System.out.println(deleteObj);
}
}
nextMarker = objectListing.getNextMarker();
} while (objectListing.isTruncated());
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.failed();
}
}
return AjaxResult.success("删除成功,共删除" + count + "个视频");
}

资源迁移OSS方案记录的更多相关文章

  1. 平台支持的从经典部署模型到 Azure Resource Manager 的 IaaS 资源迁移

    本文介绍如何才能将基础结构即服务 (IaaS) 资源从经典部署模型迁移到 Resource Manager 部署模型. 用户可以阅读有关 Azure Resource Manager 功能和优点的更多 ...

  2. Nginx+Php中限制站点目录防止跨站的配置方案记录

    Nginx+Php中限制站点目录防止跨站的配置方案记录(使用open_basedir)-------------------方法1)在Nginx配置文件中加入: 1 fastcgi_param  PH ...

  3. 基于freescale i.Mx6(ARM)的阿里云oss调试记录

    交叉编译阿里OSS调试记录 1.1 开通oss服务 具体参考以下链接: https://help.aliyun.com/document_detail/31884.html?spm=a2c4g.111 ...

  4. 服务器资源迁移到aliyun对象存储及oss的权限管理配置

    chinasoft-download增值服务的迁移和部署 需求: 增值服务网站需要从网宿迁移到阿里云,以前的增值服务历史软件存放在服务器中需要迁移到阿里云的oss中存放 需要改造程序给程序添加一个os ...

  5. openstack虚拟机迁移的操作记录

    需求说明:计算节点linux-node1.openstack:192.168.1.8  计算节点linux-node2.openstack:192.168.1.17 这两个计算节点在同一个控制节点下( ...

  6. mysql 5.7 迁移数据方案

    从一台服务器迁移至其他服务器,如何选择最短的停服时间方案 方案一.凌晨3点的全备份+停服后一天的大概一天的增备 1. 拷贝前一天的全备份至新的服务器 rsync -auzrP /Data/dbbak/ ...

  7. 如何将已部署在ASM的资源迁移到ARM中

    使用过Azure的读者都知道,Azure向客户提供了两个管理portal,一个是ASM,一个是ARM,虽然Azure官方没有宣布说淘汰ASM,两个portal可能会在很长的一段时间共存,但是考虑到AR ...

  8. UE4程序及资源加密保护方案

    UnrealEngine4外壳加密 . Virbox Protector 解决代码反汇编和反dump代码,解决软件盗版与算法抄袭. 虚幻引擎4是由游戏开发者为开发游戏而制作的.完整的游戏开发工具套件. ...

  9. WebLogic Server 多租户资源迁移

    重新建立一个动态集群,并启动,注意监听地址不能和其他集群重合 选择相应的资源组进行迁移, 迁移后,访问新的地址成功. 通过OTD负载均衡器访问原有的地址成功. 直接访问原来后台地址失败,表示资源确实已 ...

  10. nginx 部署前端资源的最佳方案

    前言 最近刚来一个运维小伙伴,做线上环境的部署的时候,前端更新资源后,总是需要清缓存才能看到个更新后的结果.客户那边也反馈更新了功能,看不到. 方案 前端小伙伴应该都知道浏览器的缓存策略,协商缓存和强 ...

随机推荐

  1. MD文本编辑工具推荐-matktext

    最开始是用vscode编辑markdown文档,左边写右边看效果的实时渲染模式,对于markdown编辑来说是多余的,多是文字类的内容,配以插图,复杂表格和脑图则更少.之后接触到Typora,所打即所 ...

  2. React:如何在普通函数中使用Hook

    解决方案

  3. React报错:You are running `create-react-app` 5.0.0, which is behind the latest release (5.0.1).

    错误 解决方案 说白了就是版本过低,升级下就好.或者按照提示卸载掉原来的版本,之后输入临时创建命令即可,如下图所示 参考链接 https://stackoverflow.com/questions/7 ...

  4. [oracle]拆分多用户的公共表空间

    前言 开发环境之前多个用户共用一个表空间,后期维护比较麻烦,因此需要将这些用户拆出来,一个用户一个表空间,以后清理这些用户也更方便. 大致思路:假设A.B.C用户共用一个表空间,将A.B.C的用户数据 ...

  5. 如何找到docker容器中的网卡外联的veth pair的另一张网卡

    1.概述 在Docker容器中,每个容器都有一个或多个网络接口(网卡),用于连接容器内部与宿主机或其他容器进行通信.这些网络接口中的一些可能是veth pair,也就是虚拟以太网对,它们以成对的方式存 ...

  6. Vue+SpringBoot项目分离部署踩坑记录

    昨天花了一晚上终于成功部署了个人网站,在这个过程中踩了很多坑,现在回顾总结记录一下,以免今后继续犯错误 前端:Vue 后端:SpringBoot 数据库:Mysql 一.前端 1.前端项目采用Ngin ...

  7. 【路由器】OpenWrt 配置使用

    目录 Web 界面 汉化 root 密码 ssh 升级 LuCI 美化 锐捷认证 MentoHUST MiniEAP 防火墙 开放端口 端口转发 IPv6 USB 安装 USB 驱动 自动挂载 Ext ...

  8. AI绘画工具MJ新功能有点东西,小白也能轻松一键换装

    先看最终做出来的效果 直接来干货吧.Midjourney,下面简称MJ 1.局部重绘功能来袭 就在前两天,MJ悄咪咪上线了这个被众人期待的新功能:局部重绘. 对于那些追求创新和个性化的设计师来说,局部 ...

  9. 《小白WEB安全入门》02. 开发篇

    @ 目录 初识HTML潜在漏洞 初识CSS潜在漏洞 初识JS潜在漏洞 初识后端潜在漏洞 后端能做什么 后端种类 后端框架 潜在漏洞 本系列文章只叙述一些超级基础理论知识,极少有实践部分 本文涉及到的语 ...

  10. LeetCode--1039

    Smiling & Weeping ----我总是躲在梦与季节的身处, 听花与黑夜唱尽梦魇, 唱尽繁华,唱断所有记忆的来路. 题目链接:1039. 多边形三角剖分的最低得分 - 力扣(Leet ...