Nest.js 配置文件上传及下载

最近使用 Nest.js 做项目,需要用到文件上传及下载功能,关于 Nest.js 的上传、下载文件一直没有仔细研究过,经过将近两天的查阅资料和动手实践,整合相关 Nest.js 上传、下载文件的相关方法。

全局配置

main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 配置 public 文件夹为静态目录,以达到可直接访问下面文件的目的
  const rootDir = join(__dirname, '..');
  app.use('/public', express.static(join(rootDir, 'public')));
  
  app.useGlobalPipes(new ValidationPipe());
  app.useGlobalFilters(new HttpExceptionFilter());
  app.useGlobalInterceptors(new TransformInterceptor());
  await app.listen(3000);
}

创建 upload 模块

nest g resource upload --no-spec

模块配置

upload.module.ts

在 Module 中添加全局配置,这种方式会将内容存储在 public/upload/ 目录下,并根据代码中配置进行判断,如果文件夹中不包含分类文件夹则自动创建

import { Module } from '@nestjs/common';
import { UploadService } from './upload.service';
import { UploadController } from './upload.controller';
import { MulterModule } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
import { checkDirAndCreate } from '../../utils/checkDirAndCreate';
const image = ['gif', 'png', 'jpg', 'jpeg', 'bmp', 'webp'];
const video = ['mp4', 'webm'];
const audio = ['mp3', 'wav', 'ogg'];
const work = ['txt', 'rtf', 'pdf', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx'];

// 配置文件上传
const multerOptions: MulterOptions = {
  // 配置文件的存储
  storage: diskStorage({
    // 存储地址
    // 配置文件上传后的文件夹路径
    destination: (req, file, cb) => {
      // 根据上传的文件类型将图片视频音频和其他类型文件分别存到对应英文文件夹
      const mimeType = file.mimetype.split('/')[1];
      let temp = 'other';
      image.filter(item => item === mimeType).length > 0
        ? (temp = 'image')
        : '';
      video.filter(item => item === mimeType).length > 0
        ? (temp = 'video')
        : '';
      audio.filter(item => item === mimeType).length > 0
        ? (temp = 'audio')
        : '';
      work.filter(item => item === mimeType).length > 0
        ? (temp = 'file')
        : '';

      const filePath = `./public/upload/${temp}/`;
      checkDirAndCreate(filePath); // 判断文件夹是否存在,不存在则自动生成
      return cb(null, `./${filePath}`);
    },
    // 存储名称
    filename: (req, file, callback) => {
      const suffix = extname(file.originalname); // 获取文件后缀
      const docName = new Date().getTime(); // 自定义文件名
      return callback(null, `${docName}${suffix}`);
    }
  }),
  // 过滤存储的文件
  fileFilter: (_req, file, callback) => {
    // multer 默认使用 latin1 编码来解析文件名, 而 latin1 编码不支持中文字符, 所以会出现中文名乱码的现象
    // 这里将文件名从 latin1 编码转换为 Buffer 对象, 再用 toString('utf8') 将 Buffer 对象转换为 utf8 编码的字符串
    // utf8 是一种支持多国语言的编码方式, 这样就可以保证文件名的中文字符不会被错误解析
    file.originalname = Buffer.from(file.originalname, 'latin1').toString(
      'utf8',
    );
    callback(null, true);
  },
  // 限制文件大小
  limits: {
    // 限制文件大小为 10 MB
    fileSize: 10 * 1024 * 1024, // 默认无限制
    // 限制文件名长度为 50 bytes
    fieldNameSize: 50, // 默认 100 bytes
  }
};

@Module({
  imports: [MulterModule.register(multerOptions),],
  controllers: [UploadController],
  providers: [UploadService]
})
export class UploadModule { }

checkDirAndCreate.ts

// src/utils/checkDirAndCreate.ts

import * as fs from ‘fs’;

export const checkDirAndCreate = (filePath: string) => {

    const pathArr = filePath.split(‘/’);

    let checkPath = ‘.’;

    let item: string;

    for (item of pathArr) {

        checkPath += `/${item}`;

        if (!fs.existsSync(checkPath)) {

            fs.mkdirSync(checkPath);

        }

    }

};

上传单个文件

import { Controller, Post, UseInterceptors ,UploadedFile} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file) {
  console.log(file);
}

现在让我们测试下,看下收到的 file 是什么样的。可以使用 postman,或者其他工具,这里不介绍如何使用工具,如果需要可在评论区评论。如下:

{
  fieldname: 'file',
  originalname: 'ceshi.png',
  encoding: '7bit',
  mimetype: 'image/png',
  buffer: <Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 07 80 00 00 04 38 08 06 00 00 00 e8 d3 c1 43 00 00 20 00 49 44 41 54 78 9c ec dd 77 9c 94 f5 bd ... 1874657 more bytes>,
  size: 1874707
}

可以看到我们获得的一个文件的详细信息,以及一个 buffer,我们可以通过收到的 buffer 进行存储,如下。

import * as fs from 'fs';

fs.writeFileSync('./hah.jpg', file.buffer);

目前为止我们已经学会如何上传一个文件以及存储。

上面这种方式,每次都需要自已写保存文件的方式,可以通过配置,来定义上传。

上传多个文件

如下,与上面的不同之处有两个 FilesInterceptorUploadedFiles

import { Controller, Post, UseInterceptors, UploadedFile, UploadedFiles } from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';

@Post('upload')
@UseInterceptors(FilesInterceptor('file'))
uploadFile(@UploadedFiles() files) {
  console.log(files);
}

日志如下:

[
  {
    fieldname: 'file',
    originalname: '1.png',
    encoding: '7bit',
    mimetype: 'image/png',
    buffer: <Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 07 80 00 00 04 38 08 06 00 00 00 e8 d3 c1 43 00 00 20 00 49 44 41 54 78 9c ec dd 7b ac 6d d9 55 ... 1770419 more bytes>,
    size: 1770469
  },
  {
    fieldname: 'file',
    originalname: '2.png',
    encoding: '7bit',
    mimetype: 'image/png',
    buffer: <Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 07 80 00 00 04 38 08 06 00 00 00 e8 d3 c1 43 00 00 20 00 49 44 41 54 78 9c ec dd 77 9c 94 f5 bd ... 1874657 more bytes>,
    size: 1874707
  }
]

自定义不同的字段名称键

@Post('upload')
@UseInterceptors(FileFieldsInterceptor([
  { name: 'avatar', maxCount: 1 },
  { name: 'background', maxCount: 1 },
]))
uploadFile(@UploadedFiles() files) {
  console.log(files);
}

也可携带其他参数

 @Post('uploads')
    @UseInterceptors(FileFieldsInterceptor([
        { name: 'avatar', maxCount: 1 },
        { name: 'background', maxCount: 1 },
        { name: 'avatar_name'},
        { name: 'background_name'}
    ]))
    async uploads(@UploadedFiles() files,@Body() body) {
        console.log(files,body)
    }

日志如下:

[Object: null prototype] {
  avatar: [
    {
      fieldname: 'avatar',
      originalname: '应用预览图-1.png',
      encoding: '7bit',
      mimetype: 'image/png',
      buffer: <Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 07 80 00 00 04 38 08 06 00 00 00 e8 d3 c1 43 00 00 20 00 49 44 41 54 78 9c ec dd 7b ac 6d d9 55 ... 1770419 more bytes>,
      size: 1770469
    }
  ],
  background: [
    {
      fieldname: 'background',
      originalname: '应用预览图-3.png',
      encoding: '7bit',
      mimetype: 'image/png',
      buffer: <Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 07 80 00 00 04 38 08 06 00 00 00 e8 d3 c1 43 00 00 20 00 49 44 41 54 78 9c ec dd 77 9c 94 f5 bd ... 1838100 more bytes>,
      size: 1838150
    }
  ]
} [Object: null prototype] {
  avatar_name: 'ceshi',
  background_name: 'ceshi'
}

上传文件使用任意的字段名称

  @Post('upload5')
  @UseInterceptors(AnyFilesInterceptor())
  uploadFileAny(@UploadedFiles() files) {
    console.log(files);
  }

参照文章

WordPress 文章转为 Markdown 格式

因为 WordPress 导出的文章是 xml 格式的文件,如果想转到其他博客平台的话非常不方便,所以想将 WordPress 的文章转换成 Markdown 格式的文件。

上网搜了一下有很多这种功能的工具,我用的是 Blogger to Markdown 这个工具。

这个工具使用很简单,如同自述文件里写的一样:

  • 下载这个项目压缩包并解压缩
  • cd 到该目录下
  • 运行 npm install 安装依赖
  • 运行 node index.js <arg>

因为我是要从 WordPress 导出为 Markdown 格式的文件,所以运行:

node index.js w your-wordpress-backup-export.xml out

稍等一会你的所有文章就都统一在 out 文件夹中生成了。

替换 Gravatar 头像

之前重新配置 WordPress 后曾经替换过 Gravatar 的头像源,但是没有想到自己用的这个 WordPress 官方主题年份已经这么旧了,竟然还有更新,导致我写入 functions.php 内的代码被覆盖掉了,我之前的头像替换就丢失了。

因为之前有搜索过关于头像的插件但都没有找到简单、易使用的,有些插件不是太重、就是太旧,夹杂的东西很多都是用不上的,有的也跟我想要的纯粹的替换头像不太一样,所以就从网上搜索,自己再次替换 functions.php 这次并作一次记录,以防后续文件覆盖后丢失。

打开 functions.php 文件,并在文件内加入如下代码:

/**
 * WordPress 头像
 */
function theme_get_avatar( $avatar ) {
	$avatar = preg_replace("/\/\/(www|\d|secure|cn).gravatar.com\/avatar\//", "//cdn.v2ex.com/gravatar/", $avatar);
	return $avatar;
}

add_filter('get_avatar', 'theme_get_avatar');

这次使用的是 V2EX 的 CDN 服务,针对国内和国外线路都有优化,而且还支持ssl访问。

同时从网上搜索了几个支持 Gravatar CDN 的网址:

Docker Mysql 8 自动定时备份

自服务器重新维护以后,上一次的 MySQL 数据库定时备份由于直接写在 crontab 中,导致这次再次测试却一直也不好使,反而导致我测试浪费了好几天。

这回从网上找到了其他方法,我今天测试了一下已经能够正常自动定时备份了。

创建备份文件

mkdir /data/backup
cd /data/backup

编写备份脚本代码

vim backup.sh
#!/bin/sh
#-h 后面改为自己的ip
#-u 后面改为自己的数据库账号
#-p 后面改为自己的数据库密码,有字符需要加""
#demand_database改为你想要备份的数据库名称
echo "开始备份数据库";
#导出所有数据库 username 替换为自己mysql登陆名,password123登陆密码
## mysqldump -h106.14.XX.XXX -uusername -p"password123" --all-databases > /data/mysqlbackup/databaseName`date +%Y-%m-%d_%H%M%S`.sql;

#导出指定数据库并压缩
## mysqldump -h106.14.XX.XXX -uusername -p"password123"  demand_database| gzip > /data/mysqlbackup/databaseName`date +%Y-%m-%d_%H%M%S`.sql.gz;

#最近转投docker怀抱,本地不安装mysql时,采用docker的mysql备份,备份语句修改为
docker exec mysql sh -c 'exec mysqldump --all-databases -uUSERNAME -pPASSWORD --all-databases' > /data/backup/database_`date +%Y-%m-%d_%H%M%S`.sql;

# mysql 替换为对应的容器名
#删除 3 天前的备份文件

backupdir=/data/backup
db_name=databaseName_

find $backupdir -name $db_name"*.sql.gz" -type f -mtime +3 -exec rm -rf {} \;


echo "备份完成";

测试以上代码需要根据自己的需求进行相应修改。

更改备份脚本权限

chmod +x dbbackup.sh

使用 crontab 定时执行备份脚本

crontab -e

输入上面的命令后会进入 vim 编辑的一个文件,在上面写 cron 表达式 + 脚本地址就行了

测试 每 1 分钟执行一次:

*/1 * * * *  /data/backup/backup.sh

我设定每 12 个小时更新一次,添加如下代码:

0 0 */12 * * /data/backup/backup.sh

参照文章

MySQL8 定时备份数据库

docker MySQL数据库的备份与还原,以及每天定时自动备份

开发一个仿朋友圈主题练手博客-Moment

很久没有发过博客了,最近在开发一个仿朋友圈主题的简陋版博客,但也没有完全完成开发,可能算是开发到了一半中的一半吧,期间捡起、放下、再捡起,需要重新看文档进行回忆,所以导致耽误了很长一段时间,即使这样,开发也不是一帆风顺,甚至说是磕磕绊绊,一个小问题可能导致耽误一天的时间。

这个项目我一般还是喜欢称它为:朋友圈,因为对于它的定位我其实也是捉摸不定的,目前权当是练手了。你可以在这里查看目前的内容:点击跳转到 Moment

目前完成了如下内容:

前端
– 昵称、头像、背景图、个性签名
– 动态显示头像、昵称(点击跳转网址链接)、内容、内容标签
– 发布天数
– 点赞
管理后台
仪表盘
– 显示发布动态数量
– 发布动态
设置
– 修改网址名称
– 修改网址头像
– 修改网址背景

这些看着很多,但其实功能都很简单,但作为刚开始学习的我来说,确实很麻烦,以至于有些地方写的很冗余、杂乱,欠缺考虑。但是也不能一直止步不前,只能在往后的时间里不断优化和完善内容了。

如果,你觉得这个项目对你来说有用处或者也想练练手,你可以选择帮忙一同开发维护,如果不介意,也可以帮忙点个 ⭐ Star。

前端:Moment

服务端:Moment Server

Docker 相关容器配置

更新时间:2023年4月9日

自从服务器重置以后,重新安装了 Docker,下面记录一下正在使用的容器和相关配置内容。

创建容器间网络

docker network create -d bridge individual

Nginx

docker run -d \
--name nginx \
-p 80:80 \
-p 443:443 \
-v $PWD/nginx/html:/usr/share/nginx/html \
-v $PWD/nginx/nginx.conf:/etc/nginx/nginx.conf \
-v $PWD/nginx/log:/var/log/nginx \
-v $PWD/nginx/cert:/etc/nginx/cert \
-v $PWD/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf \
--restart=always \
nginx

MySQL

docker run -d \
--name mysql \
-p 3306:3306 \
-v $PWD/mysql/log:/var/log/mysql \
-v $PWD/mysql/data:/var/lib/mysql \
-v $PWD/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--restart=always \
--network individual mysql --lower_case_table_names=1

MySQL 密码修改

ALTER USER 'root'@'%' IDENTIFIED WITH [mysql_native_password, caching_sha2_password] BY '123456';

MySQL 配置

进入 $PWD/mysql/conf/conf.d/ 创建 mysqld.cnf 文件,粘贴以下内容:

[mysqld]
performance_schema_max_table_instances=400
table_definition_cache=400 #缓存
performance_schema=off #用于监控MySQL server在一个较低级别的运行过程中的资源消耗、资源东西
table_open_cache=64 #打开表的缓存
innodb_buffer_pool_chunk_size=64M #InnoDB缓冲池大小调整操作的块大小
innodb_buffer_pool_size=64M #InnoDB 存储引擎的表数据和索引数据的最大内存缓冲区大小

phpMyAdmin

docker run -d \
--name phpmyadmin \
--network individual \
-e PMA_HOST="mysql" \
-p 80:80 \
--restart=always \
phpmyadmin

Redis

创建目录:
mkdir -p $PWD/redis/conf
创建文件:
touch $PWD/redis/conf/redis.conf

docker run -d \
--name redis \
-p 6379:6379 \
--network individual \
-v $PWD/redis/data:/data \
-v $PWD/redis/conf:/etc/redis \
--restart=always \
redis \
redis-server /etc/redis/redis.conf \
--appendonly yes

Warchtowe

docker run -d \
--name watchtower \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock  \
containrrr/watchtower \
--cleanup \
-i 3600

WordPress

docker run -d \
--name wordpress \
--network individual \
--restart=always \
-v $PWD/wordpress:/var/www/html \
-p 8080:80 \
wordpress

Vaultwarden

docker run -d \
--name vaultwarden \
--restart=always \
-v $PWD/vaultwarden:/data \
-p 80:80 \
vaultwarden/server:latest

QianDao

docker run -d \
--name qiandao \
--restart=always \
-p 80:80 \
-v $(pwd)/qiandao/config:/usr/src/app/config \
a76yyyy/qiandao

QingLong

docker run -dit \
--name qinglong \
--restart=always \
-p 5775:5700 \
-v $PWD/ql:/ql/data \
whyour/qinglong:latest

网站回归

嗨,欢迎回来。

可能你第一次访问,也可能你是老访客了,总之看到这里的时候说明网站的服务器前一阵子崩掉了。

说来也奇怪,凌晨两点之后服务器的监控指标突然就全部消失了,这也是服务器在崩溃后的第二天我登录服务器管理后台才发现。服务器的磁盘存储空间被塞满了,一点空间都没剩下。

记忆里,服务器磁盘空间被塞满的前一天晚我刚添加了一个青龙的 js 文件,之后就没再管理。所以我顺着这条思路排查,发现果然是青龙的日志文件把服务器空间塞满了。遂开始清理文件,恢复服务器访问。等处理好之后已经花了大半天时间,看着服务器里的文件也都存在着各种各样的错乱,内心解决了许久决定重置一下服务器,最终终究了好几天(对的,这件事是一周前发生的)终于在前天重置了服务器,直到昨天才将大部分内容重新处理好。

虽然将博客之前的文章备份了,但是恢复过来后有些设置没有变更,所以有些文章目前在搜索引擎上的路径和现在恢复后的路径有所不同,同时有些文章自己看了后觉得属于是水文,所以也稍微删除了点。

稿定设计去水印

稿定设计直接在设计当中加入水印,如何在不购买会员的情况下,取消水印呢?

方法一

我们只需要在旁边的 editor-watermark 的 CSS 样式上加上一条:

display:none !important 即可完美去除水印

注:对于有些图片,该方法已经失效。

方法二

在浏览器中打开开发者工具,定位到“元素”选项上,按住 Ctrl + F 进行搜索,搜索 editor-watermark 关键词,即可找到类名为 editor-watermark 的元素,这个就是水印元素。

然后只需要把 element.style 里面的 z-index 前面的勾给取消掉,然后把 .editor-watermark 中的 z-index 的值调整到负数,就可以完美隐藏稿定设计中的水印。

Linux 提示 You have mail.

问题描述

You have mail. 的提示

每次打开终端窗口,总是会显示 You have mail. 的提示,之前一直没有注意,这几次打开的时候才有发现。

出现这种情况的原因,是因为系统出现错误(例如 cron 出现权限问题等)需要邮件通知用户。系统会将检查的各种状态汇总,定期发送本机用户邮箱中。邮件阅读过后则不会再提示。

这个邮箱本质上是个文件,邮箱位置位于:/var/mail/用户名 里,可以使用 cat 命令查看其内容。

解决办法

删除该文件即可

sudo rm /var/mail/用户名
sudo touch /var/mail/用户名

再次打开终端窗口,You have new mail. 的提示消失。

如果一直向你发送邮件,你又不想看到可以使用以下命令:

echo "unset MAILCHECK">> /etc/profile && source /etc/profile && cat /dev/null > /var/spool/mail/root

Nest.js 使用 TypeORM 连接 MySQL 的错误问题

出现问题

使用 Nest.js 开发时,使用 TypeORM 连接 MySQL 时,配置了 TypeOrmModule.forRoot() ,却在运行项目时始终报错提示以下错误信息:

ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)…
Error: ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client

提示无法连接到数据库。

解决办法

问题在于 Node.js (mysql) 尚不支持 MySQL 8 这种新的默认身份验证方法。

一、(推荐)

mysql 更换为 mysql2

安装和使用 mysql2(而不是mysql)并使用它:

npm i mysql2
mysql = require('mysql2');

二、(不推荐)

如果只是想消除错误,以冒项目安全风险为代价(例如,这只是个人项目或开发环境)则在 MYSQL Workbench 中执行以下查询

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';

更新 root 用户配置,变更数据库加密模式。

然后运行此查询以刷新权限:

flush privileges;

这样做后尝试使用节点连接。

如果这不起作用,尝试不使用 @'localhost' 这部分。

https://stackoverflow.com/questions/50093144/mysql-8-0-client-does-not-support-authentication-protocol-requested-by-server

Stack Overflow