mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-08 07:41:10 +08:00
【新增】部署类型七牛云oss、七牛云cdn、百度cdn、腾讯waf、腾讯edgeone、阿里云waf
【新增】解析类型godaddy 【新增】自定义CA授权管理 【调整】优化部署流程,减少代码冗余,提升类型添加效率
This commit is contained in:
@@ -1,78 +0,0 @@
|
||||
# vite-plugin-git-sync
|
||||
|
||||
一个 Vite 插件,用于将构建后的文件同步到指定的 Git 仓库。
|
||||
|
||||
## 功能特点
|
||||
|
||||
- 自动检查并克隆目标 Git 仓库
|
||||
- 支持清理同步目录
|
||||
- 支持自定义文件处理函数
|
||||
- 交互式 Git 提交流程
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install vite-plugin-git-sync --save-dev
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
在 `vite.config.ts` 中配置插件:
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from "vite";
|
||||
import gitSync from "vite-plugin-git-sync";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
gitSync({
|
||||
gitUrl: "https://github.com/username/repo.git",
|
||||
syncPath: "./sync-dir",
|
||||
cleanSyncDir: true,
|
||||
fileProcessor: async (content, filePath) => {
|
||||
// 自定义文件处理逻辑
|
||||
return content;
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
| 选项 | 类型 | 必填 | 默认值 | 描述 |
|
||||
| ------------- | -------- | ---- | ------ | ------------------------------------ |
|
||||
| gitUrl | string | 是 | - | Git 仓库地址 |
|
||||
| syncPath | string | 是 | - | 同步目标目录(相对于项目根目录) |
|
||||
| cleanSyncDir | boolean | 否 | false | 是否在同步前清理目标目录 |
|
||||
| fileProcessor | function | 否 | - | 自定义文件处理函数,可以修改文件内容 |
|
||||
|
||||
## 文件处理函数
|
||||
|
||||
`fileProcessor` 函数接收两个参数:
|
||||
|
||||
- `content`: 文件内容(字符串)
|
||||
- `filePath`: 文件路径
|
||||
|
||||
返回处理后的文件内容(字符串或 Promise<string>)。
|
||||
|
||||
## 示例
|
||||
|
||||
```typescript
|
||||
// 简单的文件处理示例
|
||||
const fileProcessor = async (content: string, filePath: string) => {
|
||||
if (filePath.endsWith(".js")) {
|
||||
// 为 JS 文件添加版权信息
|
||||
return `/* Copyright ${new Date().getFullYear()} */\n${content}`;
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
// 在 vite.config.ts 中使用
|
||||
gitSync({
|
||||
gitUrl: "https://github.com/username/repo.git",
|
||||
syncPath: "./sync-dir",
|
||||
cleanSyncDir: true,
|
||||
fileProcessor,
|
||||
});
|
||||
```
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "@baota/plugin-project-sync-git",
|
||||
"version": "1.0.0",
|
||||
"description": "A Vite plugin to sync build files to a git repository",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc -w"
|
||||
},
|
||||
"keywords": [
|
||||
"vite",
|
||||
"plugin",
|
||||
"git",
|
||||
"sync"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inquirer": "^8.2.5",
|
||||
"simple-git": "^3.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/inquirer": "^8.2.5",
|
||||
"@types/node": "^20.8.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,511 +0,0 @@
|
||||
import { Plugin } from "vite";
|
||||
import { simpleGit, SimpleGit } from "simple-git";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import inquirer from "inquirer";
|
||||
import { promisify } from "util";
|
||||
import { createReadStream, createWriteStream } from "fs";
|
||||
import { pipeline } from "stream/promises";
|
||||
import { Transform } from "stream";
|
||||
|
||||
// 将 Node.js 的回调式 API 转换为 Promise 形式
|
||||
const mkdir = promisify(fs.mkdir);
|
||||
const readdir = promisify(fs.readdir);
|
||||
const stat = promisify(fs.stat);
|
||||
const rm = promisify(fs.rm);
|
||||
const exists = promisify(fs.exists);
|
||||
|
||||
/**
|
||||
* 插件配置选项接口
|
||||
*/
|
||||
export interface GitSyncOptions {
|
||||
/** Git 仓库地址,支持同步到多个仓库 */
|
||||
gitUrl: string[];
|
||||
/** 同步目标目录(相对于项目根目录)的前缀,每个仓库会在此前缀下创建对应的目录 */
|
||||
syncPath: string;
|
||||
/** 是否在同步前清理目标目录(可选) */
|
||||
cleanSyncDir?: boolean;
|
||||
/** 自定义文件处理函数,可以修改文件内容(可选) */
|
||||
fileProcessor?: (
|
||||
content: string,
|
||||
filePath: string,
|
||||
) => string | Promise<string>;
|
||||
/** 是否跳过提交确认(可选),直接进行提交 */
|
||||
skipConfirmation?: boolean;
|
||||
}
|
||||
|
||||
// 文件统计信息接口
|
||||
interface FileStats {
|
||||
size: number;
|
||||
path: string;
|
||||
type: "file" | "directory";
|
||||
children?: FileStats[];
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatFileSize(bytes: number): string {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
// 获取目录结构
|
||||
async function getDirectoryStructure(dir: string): Promise<FileStats> {
|
||||
const stats = await stat(dir);
|
||||
const result: FileStats = {
|
||||
size: stats.size,
|
||||
path: path.basename(dir),
|
||||
type: stats.isDirectory() ? "directory" : "file",
|
||||
};
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
const files = await readdir(dir);
|
||||
result.children = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const fullPath = path.join(dir, file);
|
||||
return getDirectoryStructure(fullPath);
|
||||
}),
|
||||
);
|
||||
result.size = result.children.reduce(
|
||||
(total, child) => total + child.size,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 打印目录结构
|
||||
function printDirectoryStructure(stats: FileStats, level = 0): void {
|
||||
const indent = " ".repeat(level);
|
||||
const prefix = stats.type === "directory" ? "📁" : "📄";
|
||||
console.log(
|
||||
`${indent}${prefix} ${stats.path} (${formatFileSize(stats.size)})`,
|
||||
);
|
||||
|
||||
if (stats.children) {
|
||||
stats.children.forEach((child) =>
|
||||
printDirectoryStructure(child, level + 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建进度显示流
|
||||
class ProgressStream extends Transform {
|
||||
private totalBytes = 0;
|
||||
private processedBytes = 0;
|
||||
private lastUpdate = 0;
|
||||
private readonly updateInterval = 1000; // 更新间隔(毫秒)
|
||||
|
||||
constructor(private filePath: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
_transform(
|
||||
chunk: Buffer,
|
||||
encoding: string,
|
||||
callback: (error?: Error | null, data?: Buffer) => void,
|
||||
) {
|
||||
this.processedBytes += chunk.length;
|
||||
this.totalBytes += chunk.length;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - this.lastUpdate >= this.updateInterval) {
|
||||
const progress = ((this.processedBytes / this.totalBytes) * 100).toFixed(
|
||||
2,
|
||||
);
|
||||
process.stdout.write(
|
||||
`\r处理文件 ${this.filePath}: ${progress}% (${formatFileSize(this.processedBytes)} / ${formatFileSize(this.totalBytes)})`,
|
||||
);
|
||||
this.lastUpdate = now;
|
||||
}
|
||||
|
||||
callback(null, chunk);
|
||||
}
|
||||
|
||||
_flush(callback: (error?: Error | null) => void) {
|
||||
process.stdout.write("\n");
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前项目的最新Git提交信息
|
||||
*
|
||||
* @returns 最新的Git提交信息
|
||||
*/
|
||||
async function getLatestCommitMessage(): Promise<string> {
|
||||
try {
|
||||
// 初始化当前项目的Git
|
||||
const currentProjectGit = simpleGit(process.cwd());
|
||||
|
||||
// 检查是否是Git仓库
|
||||
const isRepo = await currentProjectGit.checkIsRepo();
|
||||
if (!isRepo) {
|
||||
return "Update build files"; // 默认提交信息
|
||||
}
|
||||
|
||||
// 获取最新的提交记录
|
||||
const log = await currentProjectGit.log({ maxCount: 1 });
|
||||
if (log.latest) {
|
||||
return `Sync: ${log.latest.message}`;
|
||||
}
|
||||
|
||||
return "Update build files";
|
||||
} catch (error) {
|
||||
console.warn("获取最新提交信息失败:", error);
|
||||
return "Update build files";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个Git仓库的同步
|
||||
*
|
||||
* @param repoUrl Git仓库URL
|
||||
* @param syncBasePath 基础同步路径
|
||||
* @param distDir 构建输出目录
|
||||
* @param commitMessage 提交信息
|
||||
* @param cleanSyncDir 是否清理同步目录
|
||||
* @param fileProcessor 文件处理函数
|
||||
* @returns 同步结果
|
||||
*/
|
||||
async function syncToRepo(
|
||||
repoUrl: string,
|
||||
syncBasePath: string,
|
||||
distDir: string,
|
||||
commitMessage: string,
|
||||
cleanSyncDir: boolean,
|
||||
fileProcessor?: (
|
||||
content: string,
|
||||
filePath: string,
|
||||
) => string | Promise<string>,
|
||||
): Promise<boolean> {
|
||||
// 从仓库URL提取仓库名称作为目录名
|
||||
const repoName = path
|
||||
.basename(repoUrl, ".git")
|
||||
.replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||
const repoSyncPath = path.join(syncBasePath, repoName);
|
||||
const absoluteSyncPath = path.resolve(process.cwd(), repoSyncPath);
|
||||
|
||||
console.log(`\n开始同步到仓库: ${repoUrl}`);
|
||||
console.log(`同步目标目录: ${repoSyncPath}`);
|
||||
|
||||
let git: SimpleGit;
|
||||
|
||||
// 检查同步目录是否存在
|
||||
const syncDirExists = await exists(absoluteSyncPath);
|
||||
|
||||
if (!syncDirExists) {
|
||||
// 目录不存在,克隆仓库
|
||||
console.log(`目录 ${repoSyncPath} 不存在,正在克隆仓库...`);
|
||||
git = simpleGit();
|
||||
try {
|
||||
await git.clone(repoUrl, absoluteSyncPath);
|
||||
console.log(`仓库克隆成功: ${repoUrl}`);
|
||||
} catch (error) {
|
||||
console.error(`克隆仓库失败: ${repoUrl}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化Git
|
||||
git = simpleGit(absoluteSyncPath);
|
||||
|
||||
// 检查是否是有效的Git仓库
|
||||
try {
|
||||
const isRepo = await git.checkIsRepo();
|
||||
if (!isRepo) {
|
||||
console.error(`目录 ${repoSyncPath} 不是有效的Git仓库`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`检查Git仓库失败: ${repoSyncPath}`, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果需要清理同步目录
|
||||
if (cleanSyncDir) {
|
||||
console.log(`清理同步目录: ${repoSyncPath}`);
|
||||
const files = await readdir(absoluteSyncPath);
|
||||
for (const file of files) {
|
||||
// 保留 .git 目录,删除其他所有文件
|
||||
if (file !== ".git") {
|
||||
await rm(path.join(absoluteSyncPath, file), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 复制文件到同步目录
|
||||
try {
|
||||
await copyFiles(distDir, absoluteSyncPath, fileProcessor);
|
||||
} catch (error) {
|
||||
console.error(`复制文件失败: ${repoSyncPath}`, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 拉取远程仓库以避免冲突
|
||||
console.log(`拉取远程仓库: ${repoUrl}`);
|
||||
await git.pull();
|
||||
|
||||
// 添加所有文件
|
||||
console.log(`添加文件到Git: ${repoSyncPath}`);
|
||||
await git.add(".");
|
||||
|
||||
// 检查是否有更改
|
||||
const status = await git.status();
|
||||
if (status.files.length === 0) {
|
||||
console.log(`没有需要提交的更改: ${repoSyncPath}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 提交更改
|
||||
console.log(`提交更改: ${repoSyncPath}`);
|
||||
await git.commit(commitMessage);
|
||||
|
||||
// 推送到远程仓库
|
||||
console.log(`推送到远程仓库: ${repoUrl}`);
|
||||
await git.push();
|
||||
|
||||
console.log(`同步成功: ${repoUrl}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Git操作失败: ${repoUrl}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite 插件:将构建后的文件同步到指定的多个 Git 仓库
|
||||
*
|
||||
* 该插件作为当前项目Git之外的同步工具,主要用于将当前项目构建后的内容
|
||||
* 同步到其他Git仓库并提交。
|
||||
*
|
||||
* @param options 插件配置选项
|
||||
* @returns Vite 插件对象
|
||||
*/
|
||||
export function pluginProjectSyncGit(options: GitSyncOptions): Plugin {
|
||||
// 解构配置选项,设置默认值
|
||||
const {
|
||||
gitUrl,
|
||||
syncPath,
|
||||
cleanSyncDir = false,
|
||||
fileProcessor = undefined,
|
||||
skipConfirmation = false,
|
||||
} = options;
|
||||
|
||||
// 存储 vite 配置中的构建输出目录
|
||||
let viteBuildOutDir: string;
|
||||
|
||||
return {
|
||||
name: "vite-plugin-git-sync",
|
||||
// 仅在构建模式下应用插件
|
||||
apply: "build",
|
||||
// 在配置解析后执行,获取 vite 配置中的构建输出目录
|
||||
configResolved(config) {
|
||||
// 获取 vite 配置中的构建输出目录
|
||||
viteBuildOutDir = config.build.outDir || "dist";
|
||||
},
|
||||
// 在构建完成后执行
|
||||
async closeBundle() {
|
||||
// 使用 vite 配置中的构建输出目录
|
||||
console.log(`\n=== 项目构建同步工具 ===`);
|
||||
console.log(`使用构建输出目录: ${viteBuildOutDir}`);
|
||||
console.log(`同步目标仓库数量: ${gitUrl.length}`);
|
||||
|
||||
// 复制文件到同步目录
|
||||
const distDir = path.resolve(process.cwd(), viteBuildOutDir);
|
||||
|
||||
// 检查构建输出目录是否存在
|
||||
try {
|
||||
await stat(distDir);
|
||||
} catch {
|
||||
console.error(`构建输出目录 ${distDir} 不存在,请确保构建成功`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取默认提交信息(当前项目的最新提交信息)
|
||||
const defaultCommitMessage = await getLatestCommitMessage();
|
||||
|
||||
// 确认是否要提交
|
||||
let shouldCommit = skipConfirmation;
|
||||
let commitMessage = defaultCommitMessage;
|
||||
|
||||
if (!skipConfirmation) {
|
||||
const confirmResult = await inquirer.prompt([
|
||||
{
|
||||
type: "confirm",
|
||||
name: "shouldCommit",
|
||||
message: "是否要同步并提交更改到Git仓库?",
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
shouldCommit = confirmResult.shouldCommit;
|
||||
}
|
||||
|
||||
if (shouldCommit) {
|
||||
// 获取提交信息
|
||||
const messageResult = await inquirer.prompt([
|
||||
{
|
||||
type: "input",
|
||||
name: "commitMessage",
|
||||
message: "请输入提交信息(留空使用最新提交信息):",
|
||||
default: defaultCommitMessage,
|
||||
},
|
||||
]);
|
||||
|
||||
commitMessage = messageResult.commitMessage || defaultCommitMessage;
|
||||
|
||||
console.log(`使用提交信息: "${commitMessage}"`);
|
||||
|
||||
// 创建基础同步目录
|
||||
const absoluteSyncPath = path.resolve(process.cwd(), syncPath);
|
||||
await mkdir(absoluteSyncPath, { recursive: true });
|
||||
|
||||
// 同步到每个仓库
|
||||
const results = await Promise.all(
|
||||
gitUrl.map((url) =>
|
||||
syncToRepo(
|
||||
url,
|
||||
syncPath,
|
||||
distDir,
|
||||
commitMessage,
|
||||
cleanSyncDir,
|
||||
fileProcessor,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// 统计结果
|
||||
const successCount = results.filter(Boolean).length;
|
||||
console.log(`\n=== 同步完成 ===`);
|
||||
console.log(`成功: ${successCount}/${gitUrl.length}`);
|
||||
|
||||
if (successCount === gitUrl.length) {
|
||||
console.log("所有仓库同步成功!");
|
||||
} else {
|
||||
console.warn(`部分仓库同步失败,请检查上面的错误信息`);
|
||||
}
|
||||
} else {
|
||||
console.log("用户取消了同步操作");
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 优化的文件复制函数
|
||||
async function copyFiles(
|
||||
sourceDir: string,
|
||||
targetDir: string,
|
||||
fileProcessor?: (
|
||||
content: string,
|
||||
filePath: string,
|
||||
) => string | Promise<string>,
|
||||
) {
|
||||
console.log(`\n开始复制文件从 ${sourceDir} 到 ${targetDir}...`);
|
||||
const startTime = Date.now();
|
||||
|
||||
// 获取源目录结构
|
||||
const sourceStructure = await getDirectoryStructure(sourceDir);
|
||||
console.log("\n源目录结构:");
|
||||
printDirectoryStructure(sourceStructure);
|
||||
|
||||
// 获取源目录中的所有文件
|
||||
const files = await readdir(sourceDir);
|
||||
let totalFiles = 0;
|
||||
let processedFiles = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const sourcePath = path.join(sourceDir, file);
|
||||
const targetPath = path.join(targetDir, file);
|
||||
const stats = await stat(sourcePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
// 如果是目录,递归复制
|
||||
await mkdir(targetPath, { recursive: true });
|
||||
await copyFiles(sourcePath, targetPath, fileProcessor);
|
||||
} else {
|
||||
totalFiles++;
|
||||
try {
|
||||
// 如果是文件,根据是否提供 fileProcessor 决定处理方式
|
||||
if (fileProcessor) {
|
||||
// 使用流式处理文件内容
|
||||
const readStream = createReadStream(sourcePath, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
const writeStream = createWriteStream(targetPath, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
// 创建进度显示流
|
||||
const progressStream = new ProgressStream(sourcePath);
|
||||
|
||||
// 创建一个转换流来处理文件内容
|
||||
const transformStream = new Transform({
|
||||
transform: async (
|
||||
chunk: Buffer,
|
||||
encoding: BufferEncoding,
|
||||
callback: (error?: Error | null, data?: string) => void,
|
||||
) => {
|
||||
try {
|
||||
const processedContent = await fileProcessor(
|
||||
chunk.toString(),
|
||||
sourcePath,
|
||||
);
|
||||
callback(null, processedContent);
|
||||
} catch (error) {
|
||||
callback(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 使用 pipeline 来处理流
|
||||
await pipeline(
|
||||
readStream,
|
||||
progressStream,
|
||||
transformStream,
|
||||
writeStream,
|
||||
);
|
||||
} else {
|
||||
// 直接复制文件
|
||||
const readStream = createReadStream(sourcePath);
|
||||
const writeStream = createWriteStream(targetPath);
|
||||
const progressStream = new ProgressStream(sourcePath);
|
||||
|
||||
await pipeline(readStream, progressStream, writeStream);
|
||||
}
|
||||
|
||||
processedFiles++;
|
||||
const progress = ((processedFiles / totalFiles) * 100).toFixed(2);
|
||||
console.log(`\n总进度: ${progress}% (${processedFiles}/${totalFiles})`);
|
||||
} catch (error) {
|
||||
console.error(`\n复制文件失败: ${sourcePath} -> ${targetPath}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = (endTime - startTime) / 1000;
|
||||
console.log(`\n文件复制完成!耗时: ${duration.toFixed(2)}秒`);
|
||||
|
||||
// 获取目标目录结构
|
||||
const targetStructure = await getDirectoryStructure(targetDir);
|
||||
console.log("\n目标目录结构:");
|
||||
printDirectoryStructure(targetStructure);
|
||||
}
|
||||
|
||||
export default pluginProjectSyncGit;
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "CommonJS",
|
||||
"lib": ["ES2020"],
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user