基于Tauri、IPFS简单的网盘,软软AI中加入网盘
在Tauri中启动可执行程序,软软AI中集成IPFS中简单介绍了Tauri的项目中集成IPFS。这里简单实现下类似一个IPFS的UI,可以当成P2P网盘类使用。
首先看下实现之后的效果图
界面比较简单
简单的设计逻辑
- 文件的存储使用IPFS的能力。
- 文件的列表、目录等结构存储在sqlite数据库中
- 关于需要Pin的文件,使用IPFS Cluster的Pin功能实现(后续)
后端
sqlite 存储文件列表
sqlite 数据表设计
CREATE TABLE rrai_files (
`id` INTEGER PRIMARY KEY,
`parent_id` INTEGER DEFAULT 0,
`cid` TEXT DEFAULT '',
`is_pin` INTEGER DEFAULT 0,
`file_name` TEXT DEFAULT '',
`file_hash` TEXT DEFAULT '',
`file_type` TEXT DEFAULT '',
`category` TEXT DEFAULT '',
`avatar` TEXT DEFAULT '',
`is_dir` INTEGER DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER rrai_files_updated AFTER UPDATE ON rrai_files
BEGIN
UPDATE rrai_files SET updated_at = CURRENT_TIMESTAMP WHERE id = new.id;
END;
Tauri插件的编写
mod constants;
mod files;
mod handlers;
mod ipfs;
mod migration;
mod models;
use tauri::{
plugin::{Builder, TauriPlugin},
Runtime,
};
/// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R> {
//异步非阻塞
tauri::async_runtime::spawn(async move {
//启动 ipfs
let (mut rx, mut child) = tauri::api::process::Command::new_sidecar("ipfs")
.expect("failed to create `ipfs` binary command")
.args(["daemon"])
.spawn()
.expect("Failed to spawn sidecar");
// read events such as stdout
while let Some(event) = rx.recv().await {
if let tauri::api::process::CommandEvent::Stdout(line) = event {
tracing::debug!("IPFS:{}", line);
}
}
});
//
tauri::async_runtime::block_on(async move {
//判断是否启动 ipfs
//判断是否存在配置文件
//执行数据库脚本
tracing::debug!("执行数据库脚本");
migration::init_database().await
})
.expect("执行数据库脚本失败!");
Builder::new("rrai-storage")
.invoke_handler(tauri::generate_handler![
handlers::list_files,
handlers::list_files_by_category,
handlers::insert_file,
handlers::update_file,
handlers::delete_file,
handlers::create_dir,
handlers::ipfs_add_content,
handlers::ipfs_get_content,
])
.build()
}
提供文件的一些操作接口以及IPFS上添加和下载内容的接口。
文件列表的操作主要是sqlite的CRUD,这里不做描述。
IPFS的操作
这里使用IPFS的RUST的客户端库 ipfs-api-backend-hyper 这个库使用的是HTTP的接口。所以在启动IPFS的时候需要启动HTTP服务。这里需要注意的是不能直接使用IPFS的js的库来实现,那个依赖的node环境。
后端的代码在tauri-plugin-rrai-storage这个插件中,详细可参见github
前端
首先封装后端插件
import { invoke } from '@tauri-apps/api/tauri'
export const listFiles = async (parentId: number): Promise<FileEntity[]> => {
let res = await invoke('plugin:rrai-storage|list_files', { parentId: parentId });
console.log(res);
return res as FileEntity[];
}
export const listFilesByCategory = async (parentId: number, category: string, limit: number,): Promise<FileEntity[]> => {
let res = await invoke('plugin:rrai-storage|list_files_by_category', {
'parentId': parentId,
'category': category
});
console.log(res);
return res as FileEntity[];
}
//file_type 0 文件 1 文件夹
export const createFile = async (request: FileEntity): Promise<number> => {
let res = await invoke('plugin:rrai-storage|insert_file', {
'file': request,
});
console.log(res);
return 0;
}
export const updateFile = async (id: number, parentId: number, fileName: string, category: string, avatar: string): Promise<number> => {
let res = await invoke('plugin:rrai-storage|update_file', {
'id': id,
'parentId': parentId,
'fileName': fileName,
'category': category,
'avatar': avatar
});
console.log(res);
return 0;
}
export const deleteFile = async (fileId: number): Promise<boolean> => {
let res = await invoke('plugin:rrai-storage|delete_file', {
'id': fileId,
});
console.log(res);
return true;
}
export const createDir = async (
parentId: number,
fileName: string,
): Promise<boolean> => {
let res = await invoke('plugin:rrai-storage|create_dir', {
'parentId': parentId,
'fileName': fileName
});
console.log(res);
return true;
}
export const addContent = async (data: Uint8Array): Promise<string | null> => {
let res = await invoke('plugin:rrai-storage|ipfs_add_content', {
'data': Array.from(data),
});
console.log(res);
return res;
}
export const getContent = async (cid: string): Promise<Uint8Array | null> => {
let res = await invoke('plugin:rrai-storage|ipfs_get_content', {
'cid': cid,
});
console.log(res);
return res;
}
页面
页面的代码较为简单,可以参见github