parent
c15990d4e2
commit
eb3495fb72
@ -0,0 +1,3 @@
|
||||
target
|
||||
node_modules
|
||||
dist
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": ["prettier", "eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"overrides": [],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
target
|
||||
node_modules
|
@ -0,0 +1,3 @@
|
||||
target
|
||||
node_modules
|
||||
distr
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"rust-analyzer.cargo.features": ["sqlite"]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,15 @@
|
||||
[workspace]
|
||||
members = ["plugins/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
tauri = "1"
|
||||
serde_json = "1"
|
||||
thiserror = "1"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
authors = [ "Tauri Programme within The Commons Conservancy" ]
|
||||
license = "Apache-2.0 OR MIT"
|
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Tauri
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "plugins-workspace",
|
||||
"private": true,
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "pnpm run -r --parallel --filter !plugins-workspace build",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-terser": "^0.2.0",
|
||||
"@rollup/plugin-typescript": "^10.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.46.1",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-standard-with-typescript": "^24.0.0",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"eslint-plugin-n": "^15.0.0",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"prettier": "^2.8.1",
|
||||
"rollup": "^3.7.4",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "tauri-bindgen-host-macro"
|
||||
authors.workspace = true
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
authenticator = "0.3.1"
|
||||
once_cell = "1.9"
|
||||
sha2 = "0.10"
|
||||
base64 = { version = "^0.13" }
|
||||
u2f = "0.2"
|
||||
chrono = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
rusty-fork = "0.3"
|
@ -0,0 +1,45 @@
|
||||
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0});};
|
||||
|
||||
var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}
|
||||
|
||||
class Authenticator {
|
||||
async init() {
|
||||
return await c("plugin:authenticator|init");
|
||||
}
|
||||
async register(challenge, application) {
|
||||
return await c("plugin:authenticator|register", {
|
||||
timeout: 10000,
|
||||
challenge,
|
||||
application,
|
||||
});
|
||||
}
|
||||
async verifyRegistration(challenge, application, registerData, clientData) {
|
||||
return await c("plugin:authenticator|verify_registration", {
|
||||
challenge,
|
||||
application,
|
||||
registerData,
|
||||
clientData,
|
||||
});
|
||||
}
|
||||
async sign(challenge, application, keyHandle) {
|
||||
return await c("plugin:authenticator|sign", {
|
||||
timeout: 10000,
|
||||
challenge,
|
||||
application,
|
||||
keyHandle,
|
||||
});
|
||||
}
|
||||
async verifySignature(challenge, application, signData, clientData, keyHandle, pubkey) {
|
||||
return await c("plugin:authenticator|verify_signature", {
|
||||
challenge,
|
||||
application,
|
||||
signData,
|
||||
clientData,
|
||||
keyHandle,
|
||||
pubkey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { Authenticator };
|
||||
//# sourceMappingURL=index.min.js.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.min.js","sources":["../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-FEIY7W7S.js","../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-RCPA6UVN.js","../index.ts"],"sourcesContent":["var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0})};export{e as a};\n","import{a as d}from\"./chunk-FEIY7W7S.js\";var f={};d(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`)},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`)},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r})})}function w(e,r=\"asset\"){let n=encodeURIComponent(e);return navigator.userAgent.includes(\"Windows\")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}export{s as a,c as b,w as c,f as d};\n",null],"names":["d","invoke"],"mappings":"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"}
|
@ -0,0 +1,43 @@
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
|
||||
class Authenticator {
|
||||
async init() {
|
||||
return await invoke("plugin:authenticator|init");
|
||||
}
|
||||
async register(challenge, application) {
|
||||
return await invoke("plugin:authenticator|register", {
|
||||
timeout: 10000,
|
||||
challenge,
|
||||
application,
|
||||
});
|
||||
}
|
||||
async verifyRegistration(challenge, application, registerData, clientData) {
|
||||
return await invoke("plugin:authenticator|verify_registration", {
|
||||
challenge,
|
||||
application,
|
||||
registerData,
|
||||
clientData,
|
||||
});
|
||||
}
|
||||
async sign(challenge, application, keyHandle) {
|
||||
return await invoke("plugin:authenticator|sign", {
|
||||
timeout: 10000,
|
||||
challenge,
|
||||
application,
|
||||
keyHandle,
|
||||
});
|
||||
}
|
||||
async verifySignature(challenge, application, signData, clientData, keyHandle, pubkey) {
|
||||
return await invoke("plugin:authenticator|verify_signature", {
|
||||
challenge,
|
||||
application,
|
||||
signData,
|
||||
clientData,
|
||||
keyHandle,
|
||||
pubkey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { Authenticator };
|
||||
//# sourceMappingURL=index.mjs.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;MAEa,aAAa,CAAA;AACxB,IAAA,MAAM,IAAI,GAAA;AACR,QAAA,OAAO,MAAM,MAAM,CAAC,2BAA2B,CAAC,CAAC;KAClD;AAED,IAAA,MAAM,QAAQ,CAAC,SAAiB,EAAE,WAAmB,EAAA;AACnD,QAAA,OAAO,MAAM,MAAM,CAAC,+BAA+B,EAAE;AACnD,YAAA,OAAO,EAAE,KAAK;YACd,SAAS;YACT,WAAW;AACZ,SAAA,CAAC,CAAC;KACJ;IAED,MAAM,kBAAkB,CACtB,SAAiB,EACjB,WAAmB,EACnB,YAAoB,EACpB,UAAkB,EAAA;AAElB,QAAA,OAAO,MAAM,MAAM,CAAC,0CAA0C,EAAE;YAC9D,SAAS;YACT,WAAW;YACX,YAAY;YACZ,UAAU;AACX,SAAA,CAAC,CAAC;KACJ;AAED,IAAA,MAAM,IAAI,CACR,SAAiB,EACjB,WAAmB,EACnB,SAAiB,EAAA;AAEjB,QAAA,OAAO,MAAM,MAAM,CAAC,2BAA2B,EAAE;AAC/C,YAAA,OAAO,EAAE,KAAK;YACd,SAAS;YACT,WAAW;YACX,SAAS;AACV,SAAA,CAAC,CAAC;KACJ;AAED,IAAA,MAAM,eAAe,CACnB,SAAiB,EACjB,WAAmB,EACnB,QAAgB,EAChB,UAAkB,EAClB,SAAiB,EACjB,MAAc,EAAA;AAEd,QAAA,OAAO,MAAM,MAAM,CAAC,uCAAuC,EAAE;YAC3D,SAAS;YACT,WAAW;YACX,QAAQ;YACR,UAAU;YACV,SAAS;YACT,MAAM;AACP,SAAA,CAAC,CAAC;KACJ;AACF;;;;"}
|
@ -0,0 +1,60 @@
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
|
||||
export class Authenticator {
|
||||
async init(): Promise<void> {
|
||||
return await invoke("plugin:authenticator|init");
|
||||
}
|
||||
|
||||
async register(challenge: string, application: string): Promise<string> {
|
||||
return await invoke("plugin:authenticator|register", {
|
||||
timeout: 10000,
|
||||
challenge,
|
||||
application,
|
||||
});
|
||||
}
|
||||
|
||||
async verifyRegistration(
|
||||
challenge: string,
|
||||
application: string,
|
||||
registerData: string,
|
||||
clientData: string
|
||||
): Promise<string> {
|
||||
return await invoke("plugin:authenticator|verify_registration", {
|
||||
challenge,
|
||||
application,
|
||||
registerData,
|
||||
clientData,
|
||||
});
|
||||
}
|
||||
|
||||
async sign(
|
||||
challenge: string,
|
||||
application: string,
|
||||
keyHandle: string
|
||||
): Promise<string> {
|
||||
return await invoke("plugin:authenticator|sign", {
|
||||
timeout: 10000,
|
||||
challenge,
|
||||
application,
|
||||
keyHandle,
|
||||
});
|
||||
}
|
||||
|
||||
async verifySignature(
|
||||
challenge: string,
|
||||
application: string,
|
||||
signData: string,
|
||||
clientData: string,
|
||||
keyHandle: string,
|
||||
pubkey: string
|
||||
): Promise<number> {
|
||||
return await invoke("plugin:authenticator|verify_signature", {
|
||||
challenge,
|
||||
application,
|
||||
signData,
|
||||
clientData,
|
||||
keyHandle,
|
||||
pubkey,
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "tauri-plugin-authenticator-api",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"type": "module",
|
||||
"browser": "dist/index.min.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"browser": "./dist/index.min.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
import { createConfig } from "../../../shared/rollup.config.mjs";
|
||||
|
||||
export default createConfig({
|
||||
pkg: JSON.parse(
|
||||
readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
||||
),
|
||||
external: [/^@tauri-apps\/api/],
|
||||
});
|
@ -0,0 +1 @@
|
||||
../../../shared/tsconfig.json
|
@ -0,0 +1,217 @@
|
||||
// Copyright 2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use authenticator::{
|
||||
authenticatorservice::AuthenticatorService, statecallback::StateCallback,
|
||||
AuthenticatorTransports, KeyHandle, RegisterFlags, SignFlags, StatusUpdate,
|
||||
};
|
||||
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::io;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::{convert::Into, sync::Mutex};
|
||||
|
||||
static MANAGER: Lazy<Mutex<AuthenticatorService>> = Lazy::new(|| {
|
||||
let manager = AuthenticatorService::new().expect("The auth service should initialize safely");
|
||||
Mutex::new(manager)
|
||||
});
|
||||
|
||||
pub fn init_usb() {
|
||||
let mut manager = MANAGER.lock().unwrap();
|
||||
// theres also "add_detected_transports()" in the docs?
|
||||
manager.add_u2f_usb_hid_platform_transports();
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Registration {
|
||||
pub key_handle: String,
|
||||
pub pubkey: String,
|
||||
pub register_data: String,
|
||||
pub client_data: String,
|
||||
}
|
||||
|
||||
pub fn register(application: String, timeout: u64, challenge: String) -> crate::Result<String> {
|
||||
let (chall_bytes, app_bytes, client_data_string) =
|
||||
format_client_data(application.as_str(), challenge.as_str());
|
||||
|
||||
// log the status rx?
|
||||
let (status_tx, _status_rx) = channel::<StatusUpdate>();
|
||||
|
||||
let mut manager = MANAGER.lock().unwrap();
|
||||
|
||||
let (register_tx, register_rx) = channel();
|
||||
let callback = StateCallback::new(Box::new(move |rv| {
|
||||
register_tx.send(rv).unwrap();
|
||||
}));
|
||||
|
||||
let res = manager.register(
|
||||
RegisterFlags::empty(),
|
||||
timeout,
|
||||
chall_bytes,
|
||||
app_bytes,
|
||||
vec![],
|
||||
status_tx,
|
||||
callback,
|
||||
);
|
||||
|
||||
match res {
|
||||
Ok(_r) => {
|
||||
let register_result = register_rx
|
||||
.recv()
|
||||
.expect("Problem receiving, unable to continue");
|
||||
|
||||
if let Err(e) = register_result {
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
let (register_data, device_info) = register_result.unwrap(); // error already has been checked
|
||||
|
||||
// println!("Register result: {}", base64::encode(®ister_data));
|
||||
println!("Device info: {}", &device_info);
|
||||
|
||||
let (key_handle, public_key) =
|
||||
_u2f_get_key_handle_and_public_key_from_register_response(®ister_data).unwrap();
|
||||
let key_handle_base64 = encode_config(&key_handle, URL_SAFE_NO_PAD);
|
||||
let public_key_base64 = encode_config(&public_key, URL_SAFE_NO_PAD);
|
||||
let register_data_base64 = encode_config(®ister_data, URL_SAFE_NO_PAD);
|
||||
println!("Key Handle: {}", &key_handle_base64);
|
||||
println!("Public Key: {}", &public_key_base64);
|
||||
|
||||
// Ok(base64::encode(®ister_data))
|
||||
// Ok(key_handle_base64)
|
||||
let res = serde_json::to_string(&Registration {
|
||||
key_handle: key_handle_base64,
|
||||
pubkey: public_key_base64,
|
||||
register_data: register_data_base64,
|
||||
client_data: client_data_string,
|
||||
})?;
|
||||
Ok(res)
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Signature {
|
||||
pub key_handle: String,
|
||||
pub sign_data: String,
|
||||
}
|
||||
|
||||
pub fn sign(
|
||||
application: String,
|
||||
timeout: u64,
|
||||
challenge: String,
|
||||
key_handle: String,
|
||||
) -> crate::Result<String> {
|
||||
let credential = match decode_config(&key_handle, URL_SAFE_NO_PAD) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
let key_handle = KeyHandle {
|
||||
credential,
|
||||
transports: AuthenticatorTransports::empty(),
|
||||
};
|
||||
|
||||
let (chall_bytes, app_bytes, _) = format_client_data(application.as_str(), challenge.as_str());
|
||||
|
||||
let (sign_tx, sign_rx) = channel();
|
||||
let callback = StateCallback::new(Box::new(move |rv| {
|
||||
sign_tx.send(rv).unwrap();
|
||||
}));
|
||||
|
||||
// log the status rx?
|
||||
let (status_tx, _status_rx) = channel::<StatusUpdate>();
|
||||
|
||||
let mut manager = MANAGER.lock().unwrap();
|
||||
|
||||
let res = manager.sign(
|
||||
SignFlags::empty(),
|
||||
timeout,
|
||||
chall_bytes,
|
||||
vec![app_bytes],
|
||||
vec![key_handle],
|
||||
status_tx,
|
||||
callback,
|
||||
);
|
||||
match res {
|
||||
Ok(_v) => {
|
||||
let sign_result = sign_rx
|
||||
.recv()
|
||||
.expect("Problem receiving, unable to continue");
|
||||
|
||||
if let Err(e) = sign_result {
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
let (_, handle_used, sign_data, device_info) = sign_result.unwrap();
|
||||
|
||||
let sig = encode_config(&sign_data, URL_SAFE_NO_PAD);
|
||||
|
||||
println!("Sign result: {}", sig);
|
||||
println!(
|
||||
"Key handle used: {}",
|
||||
encode_config(&handle_used, URL_SAFE_NO_PAD)
|
||||
);
|
||||
println!("Device info: {}", &device_info);
|
||||
println!("Done.");
|
||||
|
||||
let res = serde_json::to_string(&Signature {
|
||||
sign_data: sig,
|
||||
key_handle: encode_config(&handle_used, URL_SAFE_NO_PAD),
|
||||
})?;
|
||||
Ok(res)
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_client_data(application: &str, challenge: &str) -> (Vec<u8>, Vec<u8>, String) {
|
||||
let d = format!(
|
||||
r#"{{"challenge": "{}", "version": "U2F_V2", "appId": "{}"}}"#,
|
||||
challenge, application
|
||||
);
|
||||
let mut challenge = Sha256::new();
|
||||
challenge.update(d.as_bytes());
|
||||
let chall_bytes = challenge.finalize().to_vec();
|
||||
|
||||
let mut app = Sha256::new();
|
||||
app.update(application.as_bytes());
|
||||
let app_bytes = app.finalize().to_vec();
|
||||
|
||||
(chall_bytes, app_bytes, d)
|
||||
}
|
||||
|
||||
fn _u2f_get_key_handle_and_public_key_from_register_response(
|
||||
register_response: &[u8],
|
||||
) -> io::Result<(Vec<u8>, Vec<u8>)> {
|
||||
if register_response[0] != 0x05 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Reserved byte not set correctly",
|
||||
));
|
||||
}
|
||||
|
||||
// 1: reserved
|
||||
// 65: public key
|
||||
// 1: key handle length
|
||||
// key handle
|
||||
// x.509 cert
|
||||
// sig
|
||||
|
||||
let key_handle_len = register_response[66] as usize;
|
||||
let mut public_key = register_response.to_owned();
|
||||
let mut key_handle = public_key.split_off(67);
|
||||
let _attestation = key_handle.split_off(key_handle_len);
|
||||
|
||||
// remove fist (reserved) and last (handle len) bytes
|
||||
let pk: Vec<u8> = public_key[1..public_key.len() - 1].to_vec();
|
||||
|
||||
Ok((key_handle, pk))
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Base64Decode(#[from] base64::DecodeError),
|
||||
#[error(transparent)]
|
||||
JSON(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
U2F(#[from] u2f::u2ferror::U2fError),
|
||||
#[error(transparent)]
|
||||
Auth(#[from] authenticator::errors::AuthenticatorError),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
// Copyright 2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
mod auth;
|
||||
mod error;
|
||||
mod u2f;
|
||||
|
||||
use tauri::{plugin::Plugin, Invoke, Runtime};
|
||||
|
||||
pub use error::Error;
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[tauri::command]
|
||||
fn init() {
|
||||
auth::init_usb();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn register(timeout: u64, challenge: String, application: String) -> crate::Result<String> {
|
||||
auth::register(application, timeout, challenge)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn verify_registration(
|
||||
challenge: String,
|
||||
application: String,
|
||||
register_data: String,
|
||||
client_data: String,
|
||||
) -> crate::Result<String> {
|
||||
u2f::verify_registration(application, challenge, register_data, client_data)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn sign(
|
||||
timeout: u64,
|
||||
challenge: String,
|
||||
application: String,
|
||||
key_handle: String,
|
||||
) -> crate::Result<String> {
|
||||
auth::sign(application, timeout, challenge, key_handle)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn verify_signature(
|
||||
challenge: String,
|
||||
application: String,
|
||||
sign_data: String,
|
||||
client_data: String,
|
||||
key_handle: String,
|
||||
pubkey: String,
|
||||
) -> crate::Result<u32> {
|
||||
u2f::verify_signature(
|
||||
application,
|
||||
challenge,
|
||||
sign_data,
|
||||
client_data,
|
||||
key_handle,
|
||||
pubkey,
|
||||
)
|
||||
}
|
||||
|
||||
pub struct TauriAuthenticator<R: Runtime> {
|
||||
invoke_handler: Box<dyn Fn(Invoke<R>) + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Default for TauriAuthenticator<R> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
invoke_handler: Box::new(tauri::generate_handler![
|
||||
init,
|
||||
register,
|
||||
verify_registration,
|
||||
sign,
|
||||
verify_signature
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Plugin<R> for TauriAuthenticator<R> {
|
||||
fn name(&self) -> &'static str {
|
||||
"authenticator"
|
||||
}
|
||||
|
||||
fn extend_api(&mut self, invoke: Invoke<R>) {
|
||||
(self.invoke_handler)(invoke)
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
// Copyright 2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
|
||||
use chrono::prelude::*;
|
||||
use serde::Serialize;
|
||||
use std::convert::Into;
|
||||
use u2f::messages::*;
|
||||
use u2f::protocol::*;
|
||||
use u2f::register::*;
|
||||
|
||||
static VERSION: &str = "U2F_V2";
|
||||
|
||||
pub fn make_challenge(app_id: &str, challenge_bytes: Vec<u8>) -> Challenge {
|
||||
let utc: DateTime<Utc> = Utc::now();
|
||||
Challenge {
|
||||
challenge: encode_config(&challenge_bytes, URL_SAFE_NO_PAD),
|
||||
timestamp: format!("{:?}", utc),
|
||||
app_id: app_id.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegistrationVerification {
|
||||
pub key_handle: String,
|
||||
pub pubkey: String,
|
||||
pub device_name: Option<String>,
|
||||
}
|
||||
|
||||
pub fn verify_registration(
|
||||
app_id: String,
|
||||
challenge: String,
|
||||
register_data: String,
|
||||
client_data: String,
|
||||
) -> crate::Result<String> {
|
||||
let challenge_bytes = decode_config(&challenge, URL_SAFE_NO_PAD)?;
|
||||
let challenge = make_challenge(&app_id, challenge_bytes);
|
||||
let client_data_bytes: Vec<u8> = client_data.as_bytes().into();
|
||||
let client_data_base64 = encode_config(&client_data_bytes, URL_SAFE_NO_PAD);
|
||||
let client = U2f::new(app_id);
|
||||
match client.register_response(
|
||||
challenge,
|
||||
RegisterResponse {
|
||||
registration_data: register_data,
|
||||
client_data: client_data_base64,
|
||||
version: VERSION.to_string(),
|
||||
},
|
||||
) {
|
||||
Ok(v) => {
|
||||
let rv = RegistrationVerification {
|
||||
key_handle: encode_config(&v.key_handle, URL_SAFE_NO_PAD),
|
||||
pubkey: encode_config(&v.pub_key, URL_SAFE_NO_PAD),
|
||||
device_name: v.device_name,
|
||||
};
|
||||
Ok(serde_json::to_string(&rv)?)
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SignatureVerification {
|
||||
pub counter: u8,
|
||||
}
|
||||
|
||||
pub fn verify_signature(
|
||||
app_id: String,
|
||||
challenge: String,
|
||||
sign_data: String,
|
||||
client_data: String,
|
||||
key_handle: String,
|
||||
pub_key: String,
|
||||
) -> crate::Result<u32> {
|
||||
let challenge_bytes = decode_config(&challenge, URL_SAFE_NO_PAD)?;
|
||||
let chal = make_challenge(&app_id, challenge_bytes);
|
||||
let client_data_bytes: Vec<u8> = client_data.as_bytes().into();
|
||||
let client_data_base64 = encode_config(&client_data_bytes, URL_SAFE_NO_PAD);
|
||||
let key_handle_bytes = decode_config(&key_handle, URL_SAFE_NO_PAD)?;
|
||||
let pubkey_bytes = decode_config(&pub_key, URL_SAFE_NO_PAD)?;
|
||||
let client = U2f::new(app_id);
|
||||
let mut _counter: u32 = 0;
|
||||
match client.sign_response(
|
||||
chal,
|
||||
Registration {
|
||||
// here only needs pubkey and keyhandle
|
||||
key_handle: key_handle_bytes,
|
||||
pub_key: pubkey_bytes,
|
||||
attestation_cert: None,
|
||||
device_name: None,
|
||||
},
|
||||
SignResponse {
|
||||
// here needs client data and sig data and key_handle
|
||||
signature_data: sign_data,
|
||||
client_data: client_data_base64,
|
||||
key_handle,
|
||||
},
|
||||
_counter,
|
||||
) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "tauri-plugin-autostart"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
auto-launch = "0.3"
|
@ -0,0 +1,17 @@
|
||||
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0});};
|
||||
|
||||
var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}
|
||||
|
||||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
async function isEnabled() {
|
||||
return await c("plugin:autostart|is_enabled");
|
||||
}
|
||||
async function enable() {
|
||||
await c("plugin:autostart|enable");
|
||||
}
|
||||
async function disable() {
|
||||
await c("plugin:autostart|disable");
|
||||
}
|
||||
|
||||
export { disable, enable, isEnabled };
|
||||
//# sourceMappingURL=index.min.js.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.min.js","sources":["../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-FEIY7W7S.js","../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-RCPA6UVN.js","../index.ts"],"sourcesContent":["var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0})};export{e as a};\n","import{a as d}from\"./chunk-FEIY7W7S.js\";var f={};d(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`)},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`)},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r})})}function w(e,r=\"asset\"){let n=encodeURIComponent(e);return navigator.userAgent.includes(\"Windows\")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}export{s as a,c as b,w as c,f as d};\n",null],"names":["d","invoke"],"mappings":"AAAA,IAAI,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAC,CAAC;;ACAjD,IAAI,CAAC,CAAC,EAAE,CAACA,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,OAAO,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;;ACAtuB;AAMO,eAAe,SAAS,GAAA;AAC7B,IAAA,OAAO,MAAMC,CAAM,CAAC,6BAA6B,CAAC,CAAC;AACrD,CAAC;AAEM,eAAe,MAAM,GAAA;AAC1B,IAAA,MAAMA,CAAM,CAAC,yBAAyB,CAAC,CAAC;AAC1C,CAAC;AAEM,eAAe,OAAO,GAAA;AAC3B,IAAA,MAAMA,CAAM,CAAC,0BAA0B,CAAC,CAAC;AAC3C;;;;"}
|
@ -0,0 +1,15 @@
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
|
||||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
async function isEnabled() {
|
||||
return await invoke("plugin:autostart|is_enabled");
|
||||
}
|
||||
async function enable() {
|
||||
await invoke("plugin:autostart|enable");
|
||||
}
|
||||
async function disable() {
|
||||
await invoke("plugin:autostart|disable");
|
||||
}
|
||||
|
||||
export { disable, enable, isEnabled };
|
||||
//# sourceMappingURL=index.mjs.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAAA;AAMO,eAAe,SAAS,GAAA;AAC7B,IAAA,OAAO,MAAM,MAAM,CAAC,6BAA6B,CAAC,CAAC;AACrD,CAAC;AAEM,eAAe,MAAM,GAAA;AAC1B,IAAA,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;AAC1C,CAAC;AAEM,eAAe,OAAO,GAAA;AAC3B,IAAA,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAC;AAC3C;;;;"}
|
@ -0,0 +1,17 @@
|
||||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
|
||||
export async function isEnabled(): Promise<boolean> {
|
||||
return await invoke("plugin:autostart|is_enabled");
|
||||
}
|
||||
|
||||
export async function enable(): Promise<void> {
|
||||
await invoke("plugin:autostart|enable");
|
||||
}
|
||||
|
||||
export async function disable(): Promise<void> {
|
||||
await invoke("plugin:autostart|disable");
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "tauri-plugin-autostart-api",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"type": "module",
|
||||
"browser": "dist/index.min.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"browser": "./dist/index.min.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
import { createConfig } from "../../../shared/rollup.config.mjs";
|
||||
|
||||
export default createConfig({
|
||||
pkg: JSON.parse(
|
||||
readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
||||
),
|
||||
external: [/^@tauri-apps\/api/],
|
||||
});
|
@ -0,0 +1 @@
|
||||
../../../shared/tsconfig.json
|
@ -0,0 +1,131 @@
|
||||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
use tauri::{
|
||||
command,
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Manager, Runtime, State,
|
||||
};
|
||||
|
||||
use std::env::current_exe;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum MacosLauncher {
|
||||
LaunchAgent,
|
||||
AppleScript,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("{0}")]
|
||||
Anyhow(String),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AutoLaunchManager(AutoLaunch);
|
||||
|
||||
impl AutoLaunchManager {
|
||||
pub fn enable(&self) -> Result<()> {
|
||||
self.0
|
||||
.enable()
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(Error::Anyhow)
|
||||
}
|
||||
|
||||
pub fn disable(&self) -> Result<()> {
|
||||
self.0
|
||||
.disable()
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(Error::Anyhow)
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> Result<bool> {
|
||||
self.0
|
||||
.is_enabled()
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(Error::Anyhow)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ManagerExt<R: Runtime> {
|
||||
fn autolaunch(&self) -> State<'_, AutoLaunchManager>;
|
||||
}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> ManagerExt<R> for T {
|
||||
fn autolaunch(&self) -> State<'_, AutoLaunchManager> {
|
||||
self.state::<AutoLaunchManager>()
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn enable(manager: State<'_, AutoLaunchManager>) -> Result<()> {
|
||||
manager.enable()
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn disable(manager: State<'_, AutoLaunchManager>) -> Result<()> {
|
||||
manager.disable()
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn is_enabled(manager: State<'_, AutoLaunchManager>) -> Result<bool> {
|
||||
manager.is_enabled()
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
///
|
||||
/// `args` - are passed to your app on startup.
|
||||
pub fn init<R: Runtime>(
|
||||
macos_launcher: MacosLauncher,
|
||||
args: Option<Vec<&'static str>>,
|
||||
) -> TauriPlugin<R> {
|
||||
Builder::new("autostart")
|
||||
.invoke_handler(tauri::generate_handler![enable, disable, is_enabled])
|
||||
.setup(move |app| {
|
||||
let mut builder = AutoLaunchBuilder::new();
|
||||
|
||||
builder.set_app_name(&app.package_info().name);
|
||||
if let Some(args) = args {
|
||||
builder.set_args(&args);
|
||||
}
|
||||
builder.set_use_launch_agent(matches!(macos_launcher, MacosLauncher::LaunchAgent));
|
||||
|
||||
let current_exe = current_exe()?;
|
||||
|
||||
#[cfg(windows)]
|
||||
builder.set_app_path(¤t_exe.display().to_string());
|
||||
#[cfg(target_os = "macos")]
|
||||
builder.set_app_path(¤t_exe.canonicalize()?.display().to_string());
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(appimage) = app
|
||||
.env()
|
||||
.appimage
|
||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
||||
{
|
||||
builder.set_app_path(&appimage);
|
||||
} else {
|
||||
builder.set_app_path(¤t_exe.display().to_string());
|
||||
}
|
||||
|
||||
app.manage(AutoLaunchManager(
|
||||
builder.build().map_err(|e| e.to_string())?,
|
||||
));
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "tauri-plugin-fs-extra"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
@ -0,0 +1,24 @@
|
||||
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0});};
|
||||
|
||||
var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}
|
||||
|
||||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
async function metadata(path) {
|
||||
return await c("plugin:fs-extra|metadata", {
|
||||
path,
|
||||
}).then((metadata) => {
|
||||
const { accessedAtMs, createdAtMs, modifiedAtMs, ...data } = metadata;
|
||||
return {
|
||||
accessedAt: new Date(accessedAtMs),
|
||||
createdAt: new Date(createdAtMs),
|
||||
modifiedAt: new Date(modifiedAtMs),
|
||||
...data,
|
||||
};
|
||||
});
|
||||
}
|
||||
async function exists(path) {
|
||||
return await c("plugin:fs-extra|exists", { path });
|
||||
}
|
||||
|
||||
export { exists, metadata };
|
||||
//# sourceMappingURL=index.min.js.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.min.js","sources":["../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-FEIY7W7S.js","../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-RCPA6UVN.js","../index.ts"],"sourcesContent":["var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0})};export{e as a};\n","import{a as d}from\"./chunk-FEIY7W7S.js\";var f={};d(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`)},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`)},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r})})}function w(e,r=\"asset\"){let n=encodeURIComponent(e);return navigator.userAgent.includes(\"Windows\")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}export{s as a,c as b,w as c,f as d};\n",null],"names":["d","invoke"],"mappings":"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"}
|
@ -0,0 +1,22 @@
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
|
||||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
async function metadata(path) {
|
||||
return await invoke("plugin:fs-extra|metadata", {
|
||||
path,
|
||||
}).then((metadata) => {
|
||||
const { accessedAtMs, createdAtMs, modifiedAtMs, ...data } = metadata;
|
||||
return {
|
||||
accessedAt: new Date(accessedAtMs),
|
||||
createdAt: new Date(createdAtMs),
|
||||
modifiedAt: new Date(modifiedAtMs),
|
||||
...data,
|
||||
};
|
||||
});
|
||||
}
|
||||
async function exists(path) {
|
||||
return await invoke("plugin:fs-extra|exists", { path });
|
||||
}
|
||||
|
||||
export { exists, metadata };
|
||||
//# sourceMappingURL=index.mjs.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAAA;AAiHO,eAAe,QAAQ,CAAC,IAAY,EAAA;AACzC,IAAA,OAAO,MAAM,MAAM,CAAkB,0BAA0B,EAAE;QAC/D,IAAI;AACL,KAAA,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,KAAI;AACnB,QAAA,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,IAAI,EAAE,GAAG,QAAQ,CAAC;QACtE,OAAO;AACL,YAAA,UAAU,EAAE,IAAI,IAAI,CAAC,YAAY,CAAC;AAClC,YAAA,SAAS,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC;AAChC,YAAA,UAAU,EAAE,IAAI,IAAI,CAAC,YAAY,CAAC;AAClC,YAAA,GAAG,IAAI;SACR,CAAC;AACJ,KAAC,CAAC,CAAC;AACL,CAAC;AAEM,eAAe,MAAM,CAAC,IAAY,EAAA;IACvC,OAAO,MAAM,MAAM,CAAC,wBAAwB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;AAC1D;;;;"}
|
@ -0,0 +1,130 @@
|
||||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
|
||||
export interface Permissions {
|
||||
/**
|
||||
* `true` if these permissions describe a readonly (unwritable) file.
|
||||
*/
|
||||
readonly: boolean;
|
||||
/**
|
||||
* The underlying raw `st_mode` bits that contain the standard Unix permissions for this file.
|
||||
*/
|
||||
mode: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata information about a file.
|
||||
* This structure is returned from the `metadata` function or method
|
||||
* and represents known metadata about a file such as its permissions, size, modification times, etc.
|
||||
*/
|
||||
export interface Metadata {
|
||||
/**
|
||||
* The last access time of this metadata.
|
||||
*/
|
||||
accessedAt: Date;
|
||||
/**
|
||||
* The creation time listed in this metadata.
|
||||
*/
|
||||
createdAt: Date;
|
||||
/**
|
||||
* The last modification time listed in this metadata.
|
||||
*/
|
||||
modifiedAt: Date;
|
||||
/**
|
||||
* `true` if this metadata is for a directory.
|
||||
*/
|
||||
isDir: boolean;
|
||||
/**
|
||||
* `true` if this metadata is for a regular file.
|
||||
*/
|
||||
isFile: boolean;
|
||||
/**
|
||||
* `true` if this metadata is for a symbolic link.
|
||||
*/
|
||||
isSymlink: boolean;
|
||||
/**
|
||||
* The size of the file, in bytes, this metadata is for.
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
* The permissions of the file this metadata is for.
|
||||
*/
|
||||
permissions: Permissions;
|
||||
/**
|
||||
* The ID of the device containing the file. Only available on Unix.
|
||||
*/
|
||||
dev: number | undefined;
|
||||
/**
|
||||
* The inode number. Only available on Unix.
|
||||
*/
|
||||
ino: number | undefined;
|
||||
/**
|
||||
* The rights applied to this file. Only available on Unix.
|
||||
*/
|
||||
mode: number | undefined;
|
||||
/**
|
||||
* The number of hard links pointing to this file. Only available on Unix.
|
||||
*/
|
||||
nlink: number | undefined;
|
||||
/**
|
||||
* The user ID of the owner of this file. Only available on Unix.
|
||||
*/
|
||||
uid: number | undefined;
|
||||
/**
|
||||
* The group ID of the owner of this file. Only available on Unix.
|
||||
*/
|
||||
gid: number | undefined;
|
||||
/**
|
||||
* The device ID of this file (if it is a special one). Only available on Unix.
|
||||
*/
|
||||
rdev: number | undefined;
|
||||
/**
|
||||
* The block size for filesystem I/O. Only available on Unix.
|
||||
*/
|
||||
blksize: number | undefined;
|
||||
/**
|
||||
* The number of blocks allocated to the file, in 512-byte units. Only available on Unix.
|
||||
*/
|
||||
blocks: number | undefined;
|
||||
}
|
||||
|
||||
interface BackendMetadata {
|
||||
accessedAtMs: number;
|
||||
createdAtMs: number;
|
||||
modifiedAtMs: number;
|
||||
isDir: boolean;
|
||||
isFile: boolean;
|
||||
isSymlink: boolean;
|
||||
size: number;
|
||||
permissions: Permissions;
|
||||
dev: number | undefined;
|
||||
ino: number | undefined;
|
||||
mode: number | undefined;
|
||||
nlink: number | undefined;
|
||||
uid: number | undefined;
|
||||
gid: number | undefined;
|
||||
rdev: number | undefined;
|
||||
blksize: number | undefined;
|
||||
blocks: number | undefined;
|
||||
}
|
||||
|
||||
export async function metadata(path: string): Promise<Metadata> {
|
||||
return await invoke<BackendMetadata>("plugin:fs-extra|metadata", {
|
||||
path,
|
||||
}).then((metadata) => {
|
||||
const { accessedAtMs, createdAtMs, modifiedAtMs, ...data } = metadata;
|
||||
return {
|
||||
accessedAt: new Date(accessedAtMs),
|
||||
createdAt: new Date(createdAtMs),
|
||||
modifiedAt: new Date(modifiedAtMs),
|
||||
...data,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function exists(path: string): Promise<boolean> {
|
||||
return await invoke("plugin:fs-extra|exists", { path });
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "tauri-plugin-fs-extra-api",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"type": "module",
|
||||
"browser": "dist/index.min.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"browser": "./dist/index.min.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
import { createConfig } from "../../../shared/rollup.config.mjs";
|
||||
|
||||
export default createConfig({
|
||||
pkg: JSON.parse(
|
||||
readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
||||
),
|
||||
external: [/^@tauri-apps\/api/],
|
||||
});
|
@ -0,0 +1 @@
|
||||
../../../shared/tsconfig.json
|
@ -0,0 +1,145 @@
|
||||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
use tauri::{command, plugin::Plugin, Invoke, Runtime};
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Permissions {
|
||||
readonly: bool,
|
||||
#[cfg(unix)]
|
||||
mode: u32,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UnixMetadata {
|
||||
dev: u64,
|
||||
ino: u64,
|
||||
mode: u32,
|
||||
nlink: u64,
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
rdev: u64,
|
||||
blksize: u64,
|
||||
blocks: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Metadata {
|
||||
accessed_at_ms: u64,
|
||||
created_at_ms: u64,
|
||||
modified_at_ms: u64,
|
||||
is_dir: bool,
|
||||
is_file: bool,
|
||||
is_symlink: bool,
|
||||
size: u64,
|
||||
permissions: Permissions,
|
||||
#[cfg(unix)]
|
||||
#[serde(flatten)]
|
||||
unix: UnixMetadata,
|
||||
#[cfg(windows)]
|
||||
file_attributes: u32,
|
||||
}
|
||||
|
||||
fn system_time_to_ms(time: std::io::Result<SystemTime>) -> u64 {
|
||||
time.map(|t| {
|
||||
let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap();
|
||||
duration_since_epoch.as_millis() as u64
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn metadata(path: PathBuf) -> Result<Metadata> {
|
||||
let metadata = std::fs::metadata(path)?;
|
||||
let file_type = metadata.file_type();
|
||||
let permissions = metadata.permissions();
|
||||
Ok(Metadata {
|
||||
accessed_at_ms: system_time_to_ms(metadata.accessed()),
|
||||
created_at_ms: system_time_to_ms(metadata.created()),
|
||||
modified_at_ms: system_time_to_ms(metadata.modified()),
|
||||
is_dir: file_type.is_dir(),
|
||||
is_file: file_type.is_file(),
|
||||
is_symlink: file_type.is_symlink(),
|
||||
size: metadata.len(),
|
||||
permissions: Permissions {
|
||||
readonly: permissions.readonly(),
|
||||
#[cfg(unix)]
|
||||
mode: permissions.mode(),
|
||||
},
|
||||
#[cfg(unix)]
|
||||
unix: UnixMetadata {
|
||||
dev: metadata.dev(),
|
||||
ino: metadata.ino(),
|
||||
mode: metadata.mode(),
|
||||
nlink: metadata.nlink(),
|
||||
uid: metadata.uid(),
|
||||
gid: metadata.gid(),
|
||||
rdev: metadata.rdev(),
|
||||
blksize: metadata.blksize(),
|
||||
blocks: metadata.blocks(),
|
||||
},
|
||||
#[cfg(windows)]
|
||||
file_attributes: metadata.file_attributes(),
|
||||
})
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn exists(path: PathBuf) -> bool {
|
||||
path.exists()
|
||||
}
|
||||
|
||||
/// Tauri plugin.
|
||||
pub struct FsExtra<R: Runtime> {
|
||||
invoke_handler: Box<dyn Fn(Invoke<R>) + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Default for FsExtra<R> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
invoke_handler: Box::new(tauri::generate_handler![exists, metadata]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Plugin<R> for FsExtra<R> {
|
||||
fn name(&self) -> &'static str {
|
||||
"fs-extra"
|
||||
}
|
||||
|
||||
fn extend_api(&mut self, message: Invoke<R>) {
|
||||
(self.invoke_handler)(message)
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "tauri-plugin-fs-watch"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
notify = "4.0"
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,64 @@
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
|
||||
const w = appWindow;
|
||||
async function unwatch(id) {
|
||||
await invoke("plugin:fs-watch|unwatch", { id });
|
||||
}
|
||||
async function watch(paths, options, cb) {
|
||||
const opts = {
|
||||
recursive: false,
|
||||
delayMs: 2000,
|
||||
...options,
|
||||
};
|
||||
let watchPaths;
|
||||
if (typeof paths === "string") {
|
||||
watchPaths = [paths];
|
||||
}
|
||||
else {
|
||||
watchPaths = paths;
|
||||
}
|
||||
const id = window.crypto.getRandomValues(new Uint32Array(1))[0];
|
||||
await invoke("plugin:fs-watch|watch", {
|
||||
id,
|
||||
paths: watchPaths,
|
||||
options: opts,
|
||||
});
|
||||
const unlisten = await w.listen(`watcher://debounced-event/${id}`, (event) => {
|
||||
cb(event.payload);
|
||||
});
|
||||
return () => {
|
||||
void unwatch(id);
|
||||
unlisten();
|
||||
};
|
||||
}
|
||||
async function watchImmediate(paths, options, cb) {
|
||||
const opts = {
|
||||
recursive: false,
|
||||
...options,
|
||||
delayMs: null,
|
||||
};
|
||||
let watchPaths;
|
||||
if (typeof paths === "string") {
|
||||
watchPaths = [paths];
|
||||
}
|
||||
else {
|
||||
watchPaths = paths;
|
||||
}
|
||||
const id = window.crypto.getRandomValues(new Uint32Array(1))[0];
|
||||
await invoke("plugin:fs-watch|watch", {
|
||||
id,
|
||||
paths: watchPaths,
|
||||
options: opts,
|
||||
});
|
||||
const unlisten = await w.listen(`watcher://raw-event/${id}`, (event) => {
|
||||
cb(event.payload);
|
||||
});
|
||||
return () => {
|
||||
void unwatch(id);
|
||||
unlisten();
|
||||
};
|
||||
}
|
||||
|
||||
export { watch, watchImmediate };
|
||||
//# sourceMappingURL=index.mjs.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;;AAIA,MAAM,CAAC,GAAkB,SAAS,CAAC;AA2BnC,eAAe,OAAO,CAAC,EAAU,EAAA;IAC/B,MAAM,MAAM,CAAC,yBAAyB,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;AAClD,CAAC;AAEM,eAAe,KAAK,CACzB,KAAwB,EACxB,OAA8B,EAC9B,EAAmC,EAAA;AAEnC,IAAA,MAAM,IAAI,GAAG;AACX,QAAA,SAAS,EAAE,KAAK;AAChB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,GAAG,OAAO;KACX,CAAC;AACF,IAAA,IAAI,UAAU,CAAC;AACf,IAAA,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;AAC7B,QAAA,UAAU,GAAG,CAAC,KAAK,CAAC,CAAC;AACtB,KAAA;AAAM,SAAA;QACL,UAAU,GAAG,KAAK,CAAC;AACpB,KAAA;AAED,IAAA,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEhE,MAAM,MAAM,CAAC,uBAAuB,EAAE;QACpC,EAAE;AACF,QAAA,KAAK,EAAE,UAAU;AACjB,QAAA,OAAO,EAAE,IAAI;AACd,KAAA,CAAC,CAAC;AAEH,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,MAAM,CAC7B,CAA6B,0BAAA,EAAA,EAAE,CAAE,CAAA,EACjC,CAAC,KAAK,KAAI;AACR,QAAA,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACpB,KAAC,CACF,CAAC;AAEF,IAAA,OAAO,MAAK;AACV,QAAA,KAAK,OAAO,CAAC,EAAE,CAAC,CAAC;AACjB,QAAA,QAAQ,EAAE,CAAC;AACb,KAAC,CAAC;AACJ,CAAC;AAEM,eAAe,cAAc,CAClC,KAAwB,EACxB,OAAqB,EACrB,EAA6B,EAAA;AAE7B,IAAA,MAAM,IAAI,GAAG;AACX,QAAA,SAAS,EAAE,KAAK;AAChB,QAAA,GAAG,OAAO;AACV,QAAA,OAAO,EAAE,IAAI;KACd,CAAC;AACF,IAAA,IAAI,UAAU,CAAC;AACf,IAAA,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;AAC7B,QAAA,UAAU,GAAG,CAAC,KAAK,CAAC,CAAC;AACtB,KAAA;AAAM,SAAA;QACL,UAAU,GAAG,KAAK,CAAC;AACpB,KAAA;AAED,IAAA,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEhE,MAAM,MAAM,CAAC,uBAAuB,EAAE;QACpC,EAAE;AACF,QAAA,KAAK,EAAE,UAAU;AACjB,QAAA,OAAO,EAAE,IAAI;AACd,KAAA,CAAC,CAAC;AAEH,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,MAAM,CAC7B,CAAuB,oBAAA,EAAA,EAAE,CAAE,CAAA,EAC3B,CAAC,KAAK,KAAI;AACR,QAAA,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACpB,KAAC,CACF,CAAC;AAEF,IAAA,OAAO,MAAK;AACV,QAAA,KAAK,OAAO,CAAC,EAAE,CAAC,CAAC;AACjB,QAAA,QAAQ,EAAE,CAAC;AACb,KAAC,CAAC;AACJ;;;;"}
|
@ -0,0 +1,110 @@
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { appWindow, WebviewWindow } from "@tauri-apps/api/window";
|
||||
|
||||
const w: WebviewWindow = appWindow;
|
||||
|
||||
export interface WatchOptions {
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
export interface DebouncedWatchOptions extends WatchOptions {
|
||||
delayMs?: number;
|
||||
}
|
||||
|
||||
export interface RawEvent {
|
||||
path: string | null;
|
||||
operation: number;
|
||||
cookie: number | null;
|
||||
}
|
||||
|
||||
export type DebouncedEvent =
|
||||
| { type: "NoticeWrite"; payload: string }
|
||||
| { type: "NoticeRemove"; payload: string }
|
||||
| { type: "Create"; payload: string }
|
||||
| { type: "Write"; payload: string }
|
||||
| { type: "Chmod"; payload: string }
|
||||
| { type: "Remove"; payload: string }
|
||||
| { type: "Rename"; payload: string }
|
||||
| { type: "Rescan"; payload: null }
|
||||
| { type: "Error"; payload: { error: string; path: string | null } };
|
||||
|
||||
async function unwatch(id: number): Promise<void> {
|
||||
await invoke("plugin:fs-watch|unwatch", { id });
|
||||
}
|
||||
|
||||
export async function watch(
|
||||
paths: string | string[],
|
||||
options: DebouncedWatchOptions,
|
||||
cb: (event: DebouncedEvent) => void
|
||||
): Promise<UnlistenFn> {
|
||||
const opts = {
|
||||
recursive: false,
|
||||
delayMs: 2000,
|
||||
...options,
|
||||
};
|
||||
let watchPaths;
|
||||
if (typeof paths === "string") {
|
||||
watchPaths = [paths];
|
||||
} else {
|
||||
watchPaths = paths;
|
||||
}
|
||||
|
||||
const id = window.crypto.getRandomValues(new Uint32Array(1))[0];
|
||||
|
||||
await invoke("plugin:fs-watch|watch", {
|
||||
id,
|
||||
paths: watchPaths,
|
||||
options: opts,
|
||||
});
|
||||
|
||||
const unlisten = await w.listen<DebouncedEvent>(
|
||||
`watcher://debounced-event/${id}`,
|
||||
(event) => {
|
||||
cb(event.payload);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
void unwatch(id);
|
||||
unlisten();
|
||||
};
|
||||
}
|
||||
|
||||
export async function watchImmediate(
|
||||
paths: string | string[],
|
||||
options: WatchOptions,
|
||||
cb: (event: RawEvent) => void
|
||||
): Promise<UnlistenFn> {
|
||||
const opts = {
|
||||
recursive: false,
|
||||
...options,
|
||||
delayMs: null,
|
||||
};
|
||||
let watchPaths;
|
||||
if (typeof paths === "string") {
|
||||
watchPaths = [paths];
|
||||
} else {
|
||||
watchPaths = paths;
|
||||
}
|
||||
|
||||
const id = window.crypto.getRandomValues(new Uint32Array(1))[0];
|
||||
|
||||
await invoke("plugin:fs-watch|watch", {
|
||||
id,
|
||||
paths: watchPaths,
|
||||
options: opts,
|
||||
});
|
||||
|
||||
const unlisten = await w.listen<RawEvent>(
|
||||
`watcher://raw-event/${id}`,
|
||||
(event) => {
|
||||
cb(event.payload);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
void unwatch(id);
|
||||
unlisten();
|
||||
};
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "tauri-plugin-fs-watch-api",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"type": "module",
|
||||
"browser": "dist/index.min.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"browser": "./dist/index.min.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
import { createConfig } from "../../../shared/rollup.config.mjs";
|
||||
|
||||
export default createConfig({
|
||||
pkg: JSON.parse(
|
||||
readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
||||
),
|
||||
external: [/^@tauri-apps\/api/],
|
||||
});
|
@ -0,0 +1 @@
|
||||
../../../shared/tsconfig.json
|
@ -0,0 +1,189 @@
|
||||
use notify::{
|
||||
raw_watcher, watcher, DebouncedEvent, Op, RawEvent, RecommendedWatcher, RecursiveMode,
|
||||
Watcher as _,
|
||||
};
|
||||
use serde::{ser::Serializer, Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use tauri::{command, plugin::Plugin, AppHandle, Invoke, Manager, Runtime, State, Window};
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver},
|
||||
Mutex,
|
||||
},
|
||||
thread::spawn,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
type Id = u32;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Watch(#[from] notify::Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct WatcherCollection(Mutex<HashMap<Id, (RecommendedWatcher, Vec<PathBuf>)>>);
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct RawEventWrapper {
|
||||
path: Option<PathBuf>,
|
||||
operation: u32,
|
||||
cookie: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(tag = "type", content = "payload")]
|
||||
enum DebouncedEventWrapper {
|
||||
NoticeWrite(PathBuf),
|
||||
NoticeRemove(PathBuf),
|
||||
Create(PathBuf),
|
||||
Write(PathBuf),
|
||||
Chmod(PathBuf),
|
||||
Remove(PathBuf),
|
||||
Rename(PathBuf, PathBuf),
|
||||
Rescan,
|
||||
Error {
|
||||
error: String,
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<DebouncedEvent> for DebouncedEventWrapper {
|
||||
fn from(event: DebouncedEvent) -> Self {
|
||||
match event {
|
||||
DebouncedEvent::NoticeWrite(path) => Self::NoticeWrite(path),
|
||||
DebouncedEvent::NoticeRemove(path) => Self::NoticeRemove(path),
|
||||
DebouncedEvent::Create(path) => Self::Create(path),
|
||||
DebouncedEvent::Write(path) => Self::Write(path),
|
||||
DebouncedEvent::Chmod(path) => Self::Chmod(path),
|
||||
DebouncedEvent::Remove(path) => Self::Remove(path),
|
||||
DebouncedEvent::Rename(from, to) => Self::Rename(from, to),
|
||||
DebouncedEvent::Rescan => Self::Rescan,
|
||||
DebouncedEvent::Error(error, path) => Self::Error {
|
||||
error: error.to_string(),
|
||||
path,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn watch_raw<R: Runtime>(window: Window<R>, rx: Receiver<RawEvent>, id: Id) {
|
||||
spawn(move || {
|
||||
let event_name = format!("watcher://raw-event/{}", id);
|
||||
while let Ok(event) = rx.recv() {
|
||||
let _ = window.emit(
|
||||
&event_name,
|
||||
RawEventWrapper {
|
||||
path: event.path,
|
||||
operation: event.op.unwrap_or_else(|_| Op::empty()).bits(),
|
||||
cookie: event.cookie,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn watch_debounced<R: Runtime>(window: Window<R>, rx: Receiver<DebouncedEvent>, id: Id) {
|
||||
spawn(move || {
|
||||
let event_name = format!("watcher://debounced-event/{}", id);
|
||||
while let Ok(event) = rx.recv() {
|
||||
let _ = window.emit(&event_name, DebouncedEventWrapper::from(event));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WatchOptions {
|
||||
delay_ms: Option<u64>,
|
||||
recursive: bool,
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn watch<R: Runtime>(
|
||||
window: Window<R>,
|
||||
watchers: State<'_, WatcherCollection>,
|
||||
id: Id,
|
||||
paths: Vec<PathBuf>,
|
||||
options: WatchOptions,
|
||||
) -> Result<()> {
|
||||
let mode = if options.recursive {
|
||||
RecursiveMode::Recursive
|
||||
} else {
|
||||
RecursiveMode::NonRecursive
|
||||
};
|
||||
|
||||
let watcher = if let Some(delay) = options.delay_ms {
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher = watcher(tx, Duration::from_millis(delay))?;
|
||||
for path in &paths {
|
||||
watcher.watch(path, mode)?;
|
||||
}
|
||||
watch_debounced(window, rx, id);
|
||||
watcher
|
||||
} else {
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher = raw_watcher(tx)?;
|
||||
for path in &paths {
|
||||
watcher.watch(path, mode)?;
|
||||
}
|
||||
watch_raw(window, rx, id);
|
||||
watcher
|
||||
};
|
||||
|
||||
watchers.0.lock().unwrap().insert(id, (watcher, paths));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn unwatch(watchers: State<'_, WatcherCollection>, id: Id) -> Result<()> {
|
||||
if let Some((mut watcher, paths)) = watchers.0.lock().unwrap().remove(&id) {
|
||||
for path in paths {
|
||||
watcher.unwatch(path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tauri plugin.
|
||||
pub struct Watcher<R: Runtime> {
|
||||
invoke_handler: Box<dyn Fn(Invoke<R>) + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Default for Watcher<R> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
invoke_handler: Box::new(tauri::generate_handler![watch, unwatch]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Plugin<R> for Watcher<R> {
|
||||
fn name(&self) -> &'static str {
|
||||
"fs-watch"
|
||||
}
|
||||
|
||||
fn initialize(&mut self, app: &AppHandle<R>, _config: JsonValue) -> tauri::plugin::Result<()> {
|
||||
app.manage(WatcherCollection::default());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extend_api(&mut self, message: Invoke<R>) {
|
||||
(self.invoke_handler)(message)
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "tauri-plugin-localhost"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
tiny_http = "0.11"
|
||||
http = "0.2"
|
@ -0,0 +1,118 @@
|
||||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use http::Uri;
|
||||
use tauri::{
|
||||
plugin::{Builder as PluginBuilder, TauriPlugin},
|
||||
Runtime,
|
||||
};
|
||||
use tiny_http::{Header, Response as HttpResponse, Server};
|
||||
|
||||
pub struct Request {
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn url(&self) -> &str {
|
||||
&self.url
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Response {
|
||||
headers: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn add_header<H: Into<String>, V: Into<String>>(&mut self, header: H, value: V) {
|
||||
self.headers.insert(header.into(), value.into());
|
||||
}
|
||||
}
|
||||
|
||||
type OnRequest = Option<Box<dyn Fn(&Request, &mut Response) + Send + Sync>>;
|
||||
|
||||
pub struct Builder {
|
||||
port: u16,
|
||||
on_request: OnRequest,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
pub fn new(port: u16) -> Self {
|
||||
Self {
|
||||
port,
|
||||
on_request: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_request<F: Fn(&Request, &mut Response) + Send + Sync + 'static>(
|
||||
mut self,
|
||||
f: F,
|
||||
) -> Self {
|
||||
self.on_request.replace(Box::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
|
||||
let port = self.port;
|
||||
let on_request = self.on_request.take();
|
||||
|
||||
PluginBuilder::new("localhost")
|
||||
.setup(move |app| {
|
||||
let asset_resolver = app.asset_resolver();
|
||||
std::thread::spawn(move || {
|
||||
let server = Server::http(&format!("localhost:{}", port))
|
||||
.expect("Unable to spawn server");
|
||||
for req in server.incoming_requests() {
|
||||
let path = req
|
||||
.url()
|
||||
.parse::<Uri>()
|
||||
.map(|uri| uri.path().into())
|
||||
.unwrap_or_else(|_| req.url().into());
|
||||
|
||||
#[allow(unused_mut)]
|
||||
if let Some(mut asset) = asset_resolver.get(path) {
|
||||
let request = Request {
|
||||
url: req.url().into(),
|
||||
};
|
||||
let mut response = Response {
|
||||
headers: Default::default(),
|
||||
};
|
||||
|
||||
response.add_header("Content-Type", asset.mime_type);
|
||||
if let Some(csp) = asset.csp_header {
|
||||
response
|
||||
.headers
|
||||
.insert("Content-Security-Policy".into(), csp);
|
||||
}
|
||||
|
||||
if let Some(on_request) = &on_request {
|
||||
on_request(&request, &mut response);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(response_csp) =
|
||||
response.headers.get("Content-Security-Policy")
|
||||
{
|
||||
let html = String::from_utf8_lossy(&asset.bytes);
|
||||
let body =
|
||||
html.replacen(tauri::utils::html::CSP_TOKEN, response_csp, 1);
|
||||
asset.bytes = body.as_bytes().to_vec();
|
||||
}
|
||||
|
||||
let mut resp = HttpResponse::from_data(asset.bytes);
|
||||
for (header, value) in response.headers {
|
||||
if let Ok(h) = Header::from_bytes(header.as_bytes(), value) {
|
||||
resp.add_header(h);
|
||||
}
|
||||
}
|
||||
req.respond(resp).expect("unable to setup response");
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "log"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
serde_repr = "0.1"
|
||||
byte-unit = "4.0"
|
||||
fern = "0.6"
|
||||
log = { workspace = true, features = ["kv_unstable"] }
|
||||
time = { version = "0.3", features = ["formatting"] }
|
@ -0,0 +1,176 @@
|
||||
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0});};
|
||||
|
||||
var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c$1,transformCallback:()=>s$1});function u$1(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s$1(e,r=!1){let n=u$1(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c$1(e,r={}){return new Promise((n,t)=>{let o=s$1(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s$1(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}
|
||||
|
||||
async function a(i){return c$1("tauri",i)}
|
||||
|
||||
var W={};e(W,{TauriEvent:()=>c,emit:()=>D,listen:()=>E,once:()=>_});async function s(n,t){return a({__tauriModule:"Event",message:{cmd:"unlisten",event:n,eventId:t}})}async function m(n,t,i){await a({__tauriModule:"Event",message:{cmd:"emit",event:n,windowLabel:t,payload:i}});}async function o(n,t,i){return a({__tauriModule:"Event",message:{cmd:"listen",event:n,windowLabel:t,handler:s$1(i)}}).then(r=>async()=>s(n,r))}async function u(n,t,i){return o(n,t,r=>{i(r),s(n,r.id).catch(()=>{});})}var c=(e=>(e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_CREATED="tauri://window-created",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_FILE_DROP="tauri://file-drop",e.WINDOW_FILE_DROP_HOVER="tauri://file-drop-hover",e.WINDOW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled",e.MENU="tauri://menu",e.CHECK_UPDATE="tauri://update",e.UPDATE_AVAILABLE="tauri://update-available",e.INSTALL_UPDATE="tauri://update-install",e.STATUS_UPDATE="tauri://update-status",e.DOWNLOAD_PROGRESS="tauri://update-download-progress",e))(c||{});async function E(n,t){return o(n,null,t)}async function _(n,t){return u(n,null,t)}async function D(n,t){return m(n,void 0,t)}
|
||||
|
||||
var LogLevel;
|
||||
(function (LogLevel) {
|
||||
/**
|
||||
* The "trace" level.
|
||||
*
|
||||
* Designates very low priority, often extremely verbose, information.
|
||||
*/
|
||||
LogLevel[LogLevel["Trace"] = 1] = "Trace";
|
||||
/**
|
||||
* The "debug" level.
|
||||
*
|
||||
* Designates lower priority information.
|
||||
*/
|
||||
LogLevel[LogLevel["Debug"] = 2] = "Debug";
|
||||
/**
|
||||
* The "info" level.
|
||||
*
|
||||
* Designates useful information.
|
||||
*/
|
||||
LogLevel[LogLevel["Info"] = 3] = "Info";
|
||||
/**
|
||||
* The "warn" level.
|
||||
*
|
||||
* Designates hazardous situations.
|
||||
*/
|
||||
LogLevel[LogLevel["Warn"] = 4] = "Warn";
|
||||
/**
|
||||
* The "error" level.
|
||||
*
|
||||
* Designates very serious errors.
|
||||
*/
|
||||
LogLevel[LogLevel["Error"] = 5] = "Error";
|
||||
})(LogLevel || (LogLevel = {}));
|
||||
async function log(level, message, options) {
|
||||
var _a, _b;
|
||||
const traces = (_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split("\n").map((line) => line.split("@"));
|
||||
const filtered = traces === null || traces === void 0 ? void 0 : traces.filter(([name, location]) => {
|
||||
return name.length > 0 && location !== "[native code]";
|
||||
});
|
||||
const { file, line, ...keyValues } = options !== null && options !== void 0 ? options : {};
|
||||
await c$1("plugin:log|log", {
|
||||
level,
|
||||
message,
|
||||
location: (_b = filtered === null || filtered === void 0 ? void 0 : filtered[0]) === null || _b === void 0 ? void 0 : _b.filter((v) => v.length > 0).join("@"),
|
||||
file,
|
||||
line,
|
||||
keyValues,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Logs a message at the error level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { error } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* const err_info = "No connection";
|
||||
* const port = 22;
|
||||
*
|
||||
* error(`Error: ${err_info} on port ${port}`);
|
||||
* ```
|
||||
*/
|
||||
async function error(message, options) {
|
||||
await log(LogLevel.Error, message, options);
|
||||
}
|
||||
/**
|
||||
* Logs a message at the warn level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { warn } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* const warn_description = "Invalid Input";
|
||||
*
|
||||
* warn(`Warning! {warn_description}!`);
|
||||
* ```
|
||||
*/
|
||||
async function warn(message, options) {
|
||||
await log(LogLevel.Warn, message, options);
|
||||
}
|
||||
/**
|
||||
* Logs a message at the info level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { info } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* const conn_info = { port: 40, speed: 3.20 };
|
||||
*
|
||||
* info(`Connected to port {conn_info.port} at {conn_info.speed} Mb/s`);
|
||||
* ```
|
||||
*/
|
||||
async function info(message, options) {
|
||||
await log(LogLevel.Info, message, options);
|
||||
}
|
||||
/**
|
||||
* Logs a message at the debug level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { debug } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* const pos = { x: 3.234, y: -1.223 };
|
||||
*
|
||||
* debug(`New position: x: {pos.x}, y: {pos.y}`);
|
||||
* ```
|
||||
*/
|
||||
async function debug(message, options) {
|
||||
await log(LogLevel.Debug, message, options);
|
||||
}
|
||||
/**
|
||||
* Logs a message at the trace level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { trace } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* let pos = { x: 3.234, y: -1.223 };
|
||||
*
|
||||
* trace(`Position is: x: {pos.x}, y: {pos.y}`);
|
||||
* ```
|
||||
*/
|
||||
async function trace(message, options) {
|
||||
await log(LogLevel.Trace, message, options);
|
||||
}
|
||||
async function attachConsole() {
|
||||
return await E("log://log", (event) => {
|
||||
const payload = event.payload;
|
||||
switch (payload.level) {
|
||||
case LogLevel.Trace:
|
||||
console.log(payload.message);
|
||||
break;
|
||||
case LogLevel.Debug:
|
||||
console.debug(payload.message);
|
||||
break;
|
||||
case LogLevel.Info:
|
||||
console.info(payload.message);
|
||||
break;
|
||||
case LogLevel.Warn:
|
||||
console.warn(payload.message);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
console.error(payload.message);
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`unknown log level ${payload.level}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { attachConsole, debug, error, info, trace, warn };
|
||||
//# sourceMappingURL=index.min.js.map
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,171 @@
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
|
||||
var LogLevel;
|
||||
(function (LogLevel) {
|
||||
/**
|
||||
* The "trace" level.
|
||||
*
|
||||
* Designates very low priority, often extremely verbose, information.
|
||||
*/
|
||||
LogLevel[LogLevel["Trace"] = 1] = "Trace";
|
||||
/**
|
||||
* The "debug" level.
|
||||
*
|
||||
* Designates lower priority information.
|
||||
*/
|
||||
LogLevel[LogLevel["Debug"] = 2] = "Debug";
|
||||
/**
|
||||
* The "info" level.
|
||||
*
|
||||
* Designates useful information.
|
||||
*/
|
||||
LogLevel[LogLevel["Info"] = 3] = "Info";
|
||||
/**
|
||||
* The "warn" level.
|
||||
*
|
||||
* Designates hazardous situations.
|
||||
*/
|
||||
LogLevel[LogLevel["Warn"] = 4] = "Warn";
|
||||
/**
|
||||
* The "error" level.
|
||||
*
|
||||
* Designates very serious errors.
|
||||
*/
|
||||
LogLevel[LogLevel["Error"] = 5] = "Error";
|
||||
})(LogLevel || (LogLevel = {}));
|
||||
async function log(level, message, options) {
|
||||
var _a, _b;
|
||||
const traces = (_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split("\n").map((line) => line.split("@"));
|
||||
const filtered = traces === null || traces === void 0 ? void 0 : traces.filter(([name, location]) => {
|
||||
return name.length > 0 && location !== "[native code]";
|
||||
});
|
||||
const { file, line, ...keyValues } = options !== null && options !== void 0 ? options : {};
|
||||
await invoke("plugin:log|log", {
|
||||
level,
|
||||
message,
|
||||
location: (_b = filtered === null || filtered === void 0 ? void 0 : filtered[0]) === null || _b === void 0 ? void 0 : _b.filter((v) => v.length > 0).join("@"),
|
||||
file,
|
||||
line,
|
||||
keyValues,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Logs a message at the error level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { error } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* const err_info = "No connection";
|
||||
* const port = 22;
|
||||
*
|
||||
* error(`Error: ${err_info} on port ${port}`);
|
||||
* ```
|
||||
*/
|
||||
async function error(message, options) {
|
||||
await log(LogLevel.Error, message, options);
|
||||
}
|
||||
/**
|
||||
* Logs a message at the warn level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { warn } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* const warn_description = "Invalid Input";
|
||||
*
|
||||
* warn(`Warning! {warn_description}!`);
|
||||
* ```
|
||||
*/
|
||||
async function warn(message, options) {
|
||||
await log(LogLevel.Warn, message, options);
|
||||
}
|
||||
/**
|
||||
* Logs a message at the info level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { info } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* const conn_info = { port: 40, speed: 3.20 };
|
||||
*
|
||||
* info(`Connected to port {conn_info.port} at {conn_info.speed} Mb/s`);
|
||||
* ```
|
||||
*/
|
||||
async function info(message, options) {
|
||||
await log(LogLevel.Info, message, options);
|
||||
}
|
||||
/**
|
||||
* Logs a message at the debug level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { debug } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* const pos = { x: 3.234, y: -1.223 };
|
||||
*
|
||||
* debug(`New position: x: {pos.x}, y: {pos.y}`);
|
||||
* ```
|
||||
*/
|
||||
async function debug(message, options) {
|
||||
await log(LogLevel.Debug, message, options);
|
||||
}
|
||||
/**
|
||||
* Logs a message at the trace level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { trace } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* let pos = { x: 3.234, y: -1.223 };
|
||||
*
|
||||
* trace(`Position is: x: {pos.x}, y: {pos.y}`);
|
||||
* ```
|
||||
*/
|
||||
async function trace(message, options) {
|
||||
await log(LogLevel.Trace, message, options);
|
||||
}
|
||||
async function attachConsole() {
|
||||
return await listen("log://log", (event) => {
|
||||
const payload = event.payload;
|
||||
switch (payload.level) {
|
||||
case LogLevel.Trace:
|
||||
console.log(payload.message);
|
||||
break;
|
||||
case LogLevel.Debug:
|
||||
console.debug(payload.message);
|
||||
break;
|
||||
case LogLevel.Info:
|
||||
console.info(payload.message);
|
||||
break;
|
||||
case LogLevel.Warn:
|
||||
console.warn(payload.message);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
console.error(payload.message);
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`unknown log level ${payload.level}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { attachConsole, debug, error, info, trace, warn };
|
||||
//# sourceMappingURL=index.mjs.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
@ -0,0 +1,206 @@
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||
|
||||
export type LogOptions = {
|
||||
file?: string;
|
||||
line?: number;
|
||||
} & Record<string, string | undefined>;
|
||||
|
||||
enum LogLevel {
|
||||
/**
|
||||
* The "trace" level.
|
||||
*
|
||||
* Designates very low priority, often extremely verbose, information.
|
||||
*/
|
||||
Trace = 1,
|
||||
/**
|
||||
* The "debug" level.
|
||||
*
|
||||
* Designates lower priority information.
|
||||
*/
|
||||
Debug,
|
||||
/**
|
||||
* The "info" level.
|
||||
*
|
||||
* Designates useful information.
|
||||
*/
|
||||
Info,
|
||||
/**
|
||||
* The "warn" level.
|
||||
*
|
||||
* Designates hazardous situations.
|
||||
*/
|
||||
Warn,
|
||||
/**
|
||||
* The "error" level.
|
||||
*
|
||||
* Designates very serious errors.
|
||||
*/
|
||||
Error,
|
||||
}
|
||||
|
||||
async function log(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
options?: LogOptions
|
||||
): Promise<void> {
|
||||
const traces = new Error().stack?.split("\n").map((line) => line.split("@"));
|
||||
|
||||
const filtered = traces?.filter(([name, location]) => {
|
||||
return name.length > 0 && location !== "[native code]";
|
||||
});
|
||||
|
||||
const { file, line, ...keyValues } = options ?? {};
|
||||
|
||||
await invoke("plugin:log|log", {
|
||||
level,
|
||||
message,
|
||||
location: filtered?.[0]?.filter((v) => v.length > 0).join("@"),
|
||||
file,
|
||||
line,
|
||||
keyValues,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message at the error level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { error } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* const err_info = "No connection";
|
||||
* const port = 22;
|
||||
*
|
||||
* error(`Error: ${err_info} on port ${port}`);
|
||||
* ```
|
||||
*/
|
||||
export async function error(
|
||||
message: string,
|
||||
options?: LogOptions
|
||||
): Promise<void> {
|
||||
await log(LogLevel.Error, message, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message at the warn level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { warn } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* const warn_description = "Invalid Input";
|
||||
*
|
||||
* warn(`Warning! {warn_description}!`);
|
||||
* ```
|
||||
*/
|
||||
export async function warn(
|
||||
message: string,
|
||||
options?: LogOptions
|
||||
): Promise<void> {
|
||||
await log(LogLevel.Warn, message, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message at the info level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { info } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* const conn_info = { port: 40, speed: 3.20 };
|
||||
*
|
||||
* info(`Connected to port {conn_info.port} at {conn_info.speed} Mb/s`);
|
||||
* ```
|
||||
*/
|
||||
export async function info(
|
||||
message: string,
|
||||
options?: LogOptions
|
||||
): Promise<void> {
|
||||
await log(LogLevel.Info, message, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message at the debug level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { debug } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* const pos = { x: 3.234, y: -1.223 };
|
||||
*
|
||||
* debug(`New position: x: {pos.x}, y: {pos.y}`);
|
||||
* ```
|
||||
*/
|
||||
export async function debug(
|
||||
message: string,
|
||||
options?: LogOptions
|
||||
): Promise<void> {
|
||||
await log(LogLevel.Debug, message, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message at the trace level.
|
||||
*
|
||||
* @param message
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { trace } from 'tauri-plugin-log-api';
|
||||
*
|
||||
* let pos = { x: 3.234, y: -1.223 };
|
||||
*
|
||||
* trace(`Position is: x: {pos.x}, y: {pos.y}`);
|
||||
* ```
|
||||
*/
|
||||
export async function trace(
|
||||
message: string,
|
||||
options?: LogOptions
|
||||
): Promise<void> {
|
||||
await log(LogLevel.Trace, message, options);
|
||||
}
|
||||
|
||||
interface RecordPayload {
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export async function attachConsole(): Promise<UnlistenFn> {
|
||||
return await listen("log://log", (event) => {
|
||||
const payload = event.payload as RecordPayload;
|
||||
|
||||
switch (payload.level) {
|
||||
case LogLevel.Trace:
|
||||
console.log(payload.message);
|
||||
break;
|
||||
case LogLevel.Debug:
|
||||
console.debug(payload.message);
|
||||
break;
|
||||
case LogLevel.Info:
|
||||
console.info(payload.message);
|
||||
break;
|
||||
case LogLevel.Warn:
|
||||
console.warn(payload.message);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
console.error(payload.message);
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`unknown log level ${payload.level}`);
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "tauri-plugin-log",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"type": "module",
|
||||
"browser": "dist/index.min.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"browser": "./dist/index.min.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
import { createConfig } from "../../../shared/rollup.config.mjs";
|
||||
|
||||
export default createConfig({
|
||||
pkg: JSON.parse(
|
||||
readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
||||
),
|
||||
external: [/^@tauri-apps\/api/],
|
||||
});
|
@ -0,0 +1 @@
|
||||
../../../shared/tsconfig.json
|
@ -0,0 +1,350 @@
|
||||
// Copyright 2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use fern::FormatCallback;
|
||||
use log::{logger, RecordBuilder};
|
||||
use log::{LevelFilter, Record};
|
||||
use serde::Serialize;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
fmt::Arguments,
|
||||
fs::{self, File},
|
||||
iter::FromIterator,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tauri::{
|
||||
plugin::{self, TauriPlugin},
|
||||
Manager, Runtime,
|
||||
};
|
||||
|
||||
pub use fern;
|
||||
|
||||
const DEFAULT_MAX_FILE_SIZE: u128 = 40000;
|
||||
const DEFAULT_ROTATION_STRATEGY: RotationStrategy = RotationStrategy::KeepOne;
|
||||
const DEFAULT_LOG_TARGETS: [LogTarget; 2] = [LogTarget::Stdout, LogTarget::LogDir];
|
||||
|
||||
/// An enum representing the available verbosity levels of the logger.
|
||||
///
|
||||
/// It is very similar to the [`log::Level`], but serializes to unsigned ints instead of strings.
|
||||
#[derive(Debug, Clone, Deserialize_repr, Serialize_repr)]
|
||||
#[repr(u16)]
|
||||
pub enum LogLevel {
|
||||
/// The "trace" level.
|
||||
///
|
||||
/// Designates very low priority, often extremely verbose, information.
|
||||
Trace = 1,
|
||||
/// The "debug" level.
|
||||
///
|
||||
/// Designates lower priority information.
|
||||
Debug,
|
||||
/// The "info" level.
|
||||
///
|
||||
/// Designates useful information.
|
||||
Info,
|
||||
/// The "warn" level.
|
||||
///
|
||||
/// Designates hazardous situations.
|
||||
Warn,
|
||||
/// The "error" level.
|
||||
///
|
||||
/// Designates very serious errors.
|
||||
Error,
|
||||
}
|
||||
|
||||
impl From<LogLevel> for log::Level {
|
||||
fn from(log_level: LogLevel) -> Self {
|
||||
match log_level {
|
||||
LogLevel::Trace => log::Level::Trace,
|
||||
LogLevel::Debug => log::Level::Debug,
|
||||
LogLevel::Info => log::Level::Info,
|
||||
LogLevel::Warn => log::Level::Warn,
|
||||
LogLevel::Error => log::Level::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<log::Level> for LogLevel {
|
||||
fn from(log_level: log::Level) -> Self {
|
||||
match log_level {
|
||||
log::Level::Trace => LogLevel::Trace,
|
||||
log::Level::Debug => LogLevel::Debug,
|
||||
log::Level::Info => LogLevel::Info,
|
||||
log::Level::Warn => LogLevel::Warn,
|
||||
log::Level::Error => LogLevel::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum RotationStrategy {
|
||||
KeepAll,
|
||||
KeepOne,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
struct RecordPayload {
|
||||
message: String,
|
||||
level: LogLevel,
|
||||
}
|
||||
|
||||
/// An enum representing the available targets of the logger.
|
||||
pub enum LogTarget {
|
||||
/// Print logs to stdout.
|
||||
Stdout,
|
||||
/// Print logs to stderr.
|
||||
Stderr,
|
||||
/// Write logs to the given directory.
|
||||
///
|
||||
/// The plugin will ensure the directory exists before writing logs.
|
||||
Folder(PathBuf),
|
||||
/// Write logs to the OS specififc logs directory.
|
||||
///
|
||||
/// ### Platform-specific
|
||||
///
|
||||
/// |Platform | Value | Example |
|
||||
/// | ------- | --------------------------------------------- | ---------------------------------------------- |
|
||||
/// | Linux | `{configDir}/{bundleIdentifier}` | `/home/alice/.config/com.tauri.dev` |
|
||||
/// | macOS | `{homeDir}/Library/Logs/{bundleIdentifier}` | `/Users/Alice/Library/Logs/com.tauri.dev` |
|
||||
/// | Windows | `{configDir}/{bundleIdentifier}` | `C:\Users\Alice\AppData\Roaming\com.tauri.dev` |
|
||||
LogDir,
|
||||
/// Forward logs to the webview (via the `log://log` event).
|
||||
///
|
||||
/// This requires the webview to subscribe to log events, via this plugins `attachConsole` function.
|
||||
Webview,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn log(
|
||||
level: LogLevel,
|
||||
message: String,
|
||||
location: Option<&str>,
|
||||
file: Option<&str>,
|
||||
line: Option<u32>,
|
||||
key_values: Option<HashMap<String, String>>,
|
||||
) {
|
||||
let location = location.unwrap_or("webview");
|
||||
let mut builder = RecordBuilder::new();
|
||||
builder
|
||||
.target(location)
|
||||
.level(level.into())
|
||||
.file(file)
|
||||
.line(line);
|
||||
|
||||
let key_values = key_values.unwrap_or_default();
|
||||
let mut kv = HashMap::new();
|
||||
for (k, v) in key_values.iter() {
|
||||
kv.insert(k.as_str(), v.as_str());
|
||||
}
|
||||
builder.key_values(&kv);
|
||||
|
||||
logger().log(&builder.args(format_args!("{message}")).build());
|
||||
}
|
||||
|
||||
pub struct LoggerBuilder {
|
||||
dispatch: fern::Dispatch,
|
||||
rotation_strategy: RotationStrategy,
|
||||
max_file_size: u128,
|
||||
targets: Vec<LogTarget>,
|
||||
}
|
||||
|
||||
impl Default for LoggerBuilder {
|
||||
fn default() -> Self {
|
||||
let format =
|
||||
time::format_description::parse("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]")
|
||||
.unwrap();
|
||||
let dispatch = fern::Dispatch::new().format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{}[{}][{}] {}",
|
||||
time::OffsetDateTime::now_utc().format(&format).unwrap(),
|
||||
record.target(),
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
});
|
||||
Self {
|
||||
dispatch,
|
||||
rotation_strategy: DEFAULT_ROTATION_STRATEGY,
|
||||
max_file_size: DEFAULT_MAX_FILE_SIZE,
|
||||
targets: DEFAULT_LOG_TARGETS.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LoggerBuilder {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn rotation_strategy(mut self, rotation_strategy: RotationStrategy) -> Self {
|
||||
self.rotation_strategy = rotation_strategy;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_file_size(mut self, max_file_size: u128) -> Self {
|
||||
self.max_file_size = max_file_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn format<F>(mut self, formatter: F) -> Self
|
||||
where
|
||||
F: Fn(FormatCallback, &Arguments, &Record) + Sync + Send + 'static,
|
||||
{
|
||||
self.dispatch = self.dispatch.format(formatter);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn level(mut self, level_filter: impl Into<LevelFilter>) -> Self {
|
||||
self.dispatch = self.dispatch.level(level_filter.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn level_for(mut self, module: impl Into<Cow<'static, str>>, level: LevelFilter) -> Self {
|
||||
self.dispatch = self.dispatch.level_for(module, level);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn filter<F>(mut self, filter: F) -> Self
|
||||
where
|
||||
F: Fn(&log::Metadata) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
self.dispatch = self.dispatch.filter(filter);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn target(mut self, target: LogTarget) -> Self {
|
||||
self.targets.push(target);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn targets(mut self, targets: impl IntoIterator<Item = LogTarget>) -> Self {
|
||||
self.targets = Vec::from_iter(targets);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "colored")]
|
||||
pub fn with_colors(self, colors: fern::colors::ColoredLevelConfig) -> Self {
|
||||
let format =
|
||||
time::format_description::parse("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]")
|
||||
.unwrap();
|
||||
self.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{}[{}][{}] {}",
|
||||
time::OffsetDateTime::now_utc().format(&format).unwrap(),
|
||||
record.target(),
|
||||
colors.color(record.level()),
|
||||
message
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
|
||||
plugin::Builder::new("log")
|
||||
.invoke_handler(tauri::generate_handler![log])
|
||||
.setup(move |app_handle| {
|
||||
let app_name = &app_handle.package_info().name;
|
||||
|
||||
// setup targets
|
||||
for target in &self.targets {
|
||||
self.dispatch = self.dispatch.chain(match target {
|
||||
LogTarget::Stdout => fern::Output::from(std::io::stdout()),
|
||||
LogTarget::Stderr => fern::Output::from(std::io::stderr()),
|
||||
LogTarget::Folder(path) => {
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(&path).unwrap();
|
||||
}
|
||||
|
||||
fern::log_file(get_log_file_path(
|
||||
&path,
|
||||
app_name,
|
||||
&self.rotation_strategy,
|
||||
self.max_file_size,
|
||||
)?)?
|
||||
.into()
|
||||
}
|
||||
LogTarget::LogDir => {
|
||||
let path = app_handle.path_resolver().log_dir().unwrap();
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(&path).unwrap();
|
||||
}
|
||||
|
||||
fern::log_file(get_log_file_path(
|
||||
&path,
|
||||
app_name,
|
||||
&self.rotation_strategy,
|
||||
self.max_file_size,
|
||||
)?)?
|
||||
.into()
|
||||
}
|
||||
LogTarget::Webview => {
|
||||
let app_handle = app_handle.clone();
|
||||
|
||||
fern::Output::call(move |record| {
|
||||
let payload = RecordPayload {
|
||||
message: record.args().to_string(),
|
||||
level: record.level().into(),
|
||||
};
|
||||
let app_handle = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
app_handle.emit_all("log://log", payload).unwrap();
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.dispatch.apply()?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_log_file_path(
|
||||
dir: &impl AsRef<Path>,
|
||||
app_name: &str,
|
||||
rotation_strategy: &RotationStrategy,
|
||||
max_file_size: u128,
|
||||
) -> plugin::Result<PathBuf> {
|
||||
let path = dir.as_ref().join(format!("{}.log", app_name));
|
||||
|
||||
if path.exists() {
|
||||
let log_size = File::open(&path)?.metadata()?.len() as u128;
|
||||
if log_size > max_file_size {
|
||||
match rotation_strategy {
|
||||
RotationStrategy::KeepAll => {
|
||||
let to = dir.as_ref().join(format!(
|
||||
"{}_{}.log",
|
||||
app_name,
|
||||
time::OffsetDateTime::now_utc()
|
||||
.format(
|
||||
&time::format_description::parse(
|
||||
"[year]-[month]-[day]_[hour]-[minute]-[second]"
|
||||
)
|
||||
.unwrap()
|
||||
)
|
||||
.unwrap(),
|
||||
));
|
||||
if to.is_file() {
|
||||
// designated rotated log file name already exists
|
||||
// highly unlikely but defensively handle anyway by adding .bak to filename
|
||||
let mut to_bak = to.clone();
|
||||
to_bak.set_file_name(format!(
|
||||
"{}.bak",
|
||||
to_bak.file_name().unwrap().to_string_lossy()
|
||||
));
|
||||
fs::rename(&to, to_bak)?;
|
||||
}
|
||||
fs::rename(&path, to)?;
|
||||
}
|
||||
RotationStrategy::KeepOne => {
|
||||
fs::remove_file(&path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "tauri-plugin-persisted-scope"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
bincode = "1"
|
||||
|
||||
[features]
|
||||
protocol-asset = [ "tauri/protocol-asset" ]
|
@ -0,0 +1,101 @@
|
||||
// Copyright 2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
FsScopeEvent, Manager, Runtime,
|
||||
};
|
||||
|
||||
use std::{
|
||||
fs::{create_dir_all, File},
|
||||
io::Write,
|
||||
};
|
||||
|
||||
const SCOPE_STATE_FILENAME: &str = ".persisted-scope";
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Tauri(#[from] tauri::Error),
|
||||
#[error(transparent)]
|
||||
TauriApi(#[from] tauri::api::Error),
|
||||
#[error(transparent)]
|
||||
Bincode(#[from] Box<bincode::ErrorKind>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
struct Scope {
|
||||
allowed_paths: Vec<String>,
|
||||
forbidden_patterns: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("persisted-scope")
|
||||
.setup(|app| {
|
||||
let fs_scope = app.fs_scope();
|
||||
#[cfg(feature = "protocol-asset")]
|
||||
let asset_protocol_scope = app.asset_protocol_scope();
|
||||
let app = app.clone();
|
||||
let app_dir = app.path_resolver().app_dir();
|
||||
|
||||
if let Some(app_dir) = app_dir {
|
||||
let scope_state_path = app_dir.join(SCOPE_STATE_FILENAME);
|
||||
|
||||
let _ = fs_scope.forbid_file(&scope_state_path);
|
||||
#[cfg(feature = "protocol-asset")]
|
||||
let _ = asset_protocol_scope.forbid_file(&scope_state_path);
|
||||
|
||||
if scope_state_path.exists() {
|
||||
let scope: Scope = tauri::api::file::read_binary(&scope_state_path)
|
||||
.map_err(Error::from)
|
||||
.and_then(|scope| bincode::deserialize(&scope).map_err(Into::into))
|
||||
.unwrap_or_default();
|
||||
for allowed in scope.allowed_paths {
|
||||
// allows the path as is
|
||||
let _ = fs_scope.allow_file(&allowed);
|
||||
#[cfg(feature = "protocol-asset")]
|
||||
let _ = asset_protocol_scope.allow_file(allowed);
|
||||
}
|
||||
for forbidden in scope.forbidden_patterns {
|
||||
// forbid the path as is
|
||||
let _ = fs_scope.forbid_file(&forbidden);
|
||||
#[cfg(feature = "protocol-asset")]
|
||||
let _ = asset_protocol_scope.forbid_file(forbidden);
|
||||
}
|
||||
}
|
||||
|
||||
fs_scope.listen(move |event| {
|
||||
let fs_scope = app.fs_scope();
|
||||
if let FsScopeEvent::PathAllowed(_) = event {
|
||||
let scope = Scope {
|
||||
allowed_paths: fs_scope
|
||||
.allowed_patterns()
|
||||
.into_iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect(),
|
||||
forbidden_patterns: fs_scope
|
||||
.forbidden_patterns()
|
||||
.into_iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect(),
|
||||
};
|
||||
let scope_state_path = scope_state_path.clone();
|
||||
|
||||
let _ = create_dir_all(&app_dir)
|
||||
.and_then(|_| File::create(scope_state_path))
|
||||
.map_err(Error::Io)
|
||||
.and_then(|mut f| {
|
||||
f.write_all(&bincode::serialize(&scope).map_err(Error::from)?)
|
||||
.map_err(Into::into)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "tauri-plugin-positioner"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[features]
|
||||
system-tray = [ "tauri/system-tray" ]
|
@ -0,0 +1,40 @@
|
||||
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0});};
|
||||
|
||||
var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}
|
||||
|
||||
// Copyright 2021 Jonas Kruckenberg
|
||||
/**
|
||||
* Well known window positions.
|
||||
*/
|
||||
var Position;
|
||||
(function (Position) {
|
||||
Position[Position["TopLeft"] = 0] = "TopLeft";
|
||||
Position[Position["TopRight"] = 1] = "TopRight";
|
||||
Position[Position["BottomLeft"] = 2] = "BottomLeft";
|
||||
Position[Position["BottomRight"] = 3] = "BottomRight";
|
||||
Position[Position["TopCenter"] = 4] = "TopCenter";
|
||||
Position[Position["BottomCenter"] = 5] = "BottomCenter";
|
||||
Position[Position["LeftCenter"] = 6] = "LeftCenter";
|
||||
Position[Position["RightCenter"] = 7] = "RightCenter";
|
||||
Position[Position["Center"] = 8] = "Center";
|
||||
Position[Position["TrayLeft"] = 9] = "TrayLeft";
|
||||
Position[Position["TrayBottomLeft"] = 10] = "TrayBottomLeft";
|
||||
Position[Position["TrayRight"] = 11] = "TrayRight";
|
||||
Position[Position["TrayBottomRight"] = 12] = "TrayBottomRight";
|
||||
Position[Position["TrayCenter"] = 13] = "TrayCenter";
|
||||
Position[Position["TrayBottomCenter"] = 14] = "TrayBottomCenter";
|
||||
})(Position || (Position = {}));
|
||||
/**
|
||||
* Moves the `Window` to the given {@link Position} using `WindowExt.move_window()`
|
||||
* All positions are relative to the **current** screen.
|
||||
*
|
||||
* @param to The {@link Position} to move to.
|
||||
*/
|
||||
async function moveWindow(to) {
|
||||
await c("plugin:positioner|move_window", {
|
||||
position: to,
|
||||
});
|
||||
}
|
||||
|
||||
export { Position, moveWindow };
|
||||
//# sourceMappingURL=index.min.js.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.min.js","sources":["../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-FEIY7W7S.js","../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-RCPA6UVN.js","../index.ts"],"sourcesContent":["var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0})};export{e as a};\n","import{a as d}from\"./chunk-FEIY7W7S.js\";var f={};d(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`)},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`)},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r})})}function w(e,r=\"asset\"){let n=encodeURIComponent(e);return navigator.userAgent.includes(\"Windows\")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}export{s as a,c as b,w as c,f as d};\n",null],"names":["d","invoke"],"mappings":"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"}
|
@ -0,0 +1,38 @@
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
|
||||
// Copyright 2021 Jonas Kruckenberg
|
||||
/**
|
||||
* Well known window positions.
|
||||
*/
|
||||
var Position;
|
||||
(function (Position) {
|
||||
Position[Position["TopLeft"] = 0] = "TopLeft";
|
||||
Position[Position["TopRight"] = 1] = "TopRight";
|
||||
Position[Position["BottomLeft"] = 2] = "BottomLeft";
|
||||
Position[Position["BottomRight"] = 3] = "BottomRight";
|
||||
Position[Position["TopCenter"] = 4] = "TopCenter";
|
||||
Position[Position["BottomCenter"] = 5] = "BottomCenter";
|
||||
Position[Position["LeftCenter"] = 6] = "LeftCenter";
|
||||
Position[Position["RightCenter"] = 7] = "RightCenter";
|
||||
Position[Position["Center"] = 8] = "Center";
|
||||
Position[Position["TrayLeft"] = 9] = "TrayLeft";
|
||||
Position[Position["TrayBottomLeft"] = 10] = "TrayBottomLeft";
|
||||
Position[Position["TrayRight"] = 11] = "TrayRight";
|
||||
Position[Position["TrayBottomRight"] = 12] = "TrayBottomRight";
|
||||
Position[Position["TrayCenter"] = 13] = "TrayCenter";
|
||||
Position[Position["TrayBottomCenter"] = 14] = "TrayBottomCenter";
|
||||
})(Position || (Position = {}));
|
||||
/**
|
||||
* Moves the `Window` to the given {@link Position} using `WindowExt.move_window()`
|
||||
* All positions are relative to the **current** screen.
|
||||
*
|
||||
* @param to The {@link Position} to move to.
|
||||
*/
|
||||
async function moveWindow(to) {
|
||||
await invoke("plugin:positioner|move_window", {
|
||||
position: to,
|
||||
});
|
||||
}
|
||||
|
||||
export { Position, moveWindow };
|
||||
//# sourceMappingURL=index.mjs.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAAA;AAKA;;AAEG;IACS,SAgBX;AAhBD,CAAA,UAAY,QAAQ,EAAA;AAClB,IAAA,QAAA,CAAA,QAAA,CAAA,SAAA,CAAA,GAAA,CAAA,CAAA,GAAA,SAAW,CAAA;AACX,IAAA,QAAA,CAAA,QAAA,CAAA,UAAA,CAAA,GAAA,CAAA,CAAA,GAAA,UAAQ,CAAA;AACR,IAAA,QAAA,CAAA,QAAA,CAAA,YAAA,CAAA,GAAA,CAAA,CAAA,GAAA,YAAU,CAAA;AACV,IAAA,QAAA,CAAA,QAAA,CAAA,aAAA,CAAA,GAAA,CAAA,CAAA,GAAA,aAAW,CAAA;AACX,IAAA,QAAA,CAAA,QAAA,CAAA,WAAA,CAAA,GAAA,CAAA,CAAA,GAAA,WAAS,CAAA;AACT,IAAA,QAAA,CAAA,QAAA,CAAA,cAAA,CAAA,GAAA,CAAA,CAAA,GAAA,cAAY,CAAA;AACZ,IAAA,QAAA,CAAA,QAAA,CAAA,YAAA,CAAA,GAAA,CAAA,CAAA,GAAA,YAAU,CAAA;AACV,IAAA,QAAA,CAAA,QAAA,CAAA,aAAA,CAAA,GAAA,CAAA,CAAA,GAAA,aAAW,CAAA;AACX,IAAA,QAAA,CAAA,QAAA,CAAA,QAAA,CAAA,GAAA,CAAA,CAAA,GAAA,QAAM,CAAA;AACN,IAAA,QAAA,CAAA,QAAA,CAAA,UAAA,CAAA,GAAA,CAAA,CAAA,GAAA,UAAQ,CAAA;AACR,IAAA,QAAA,CAAA,QAAA,CAAA,gBAAA,CAAA,GAAA,EAAA,CAAA,GAAA,gBAAc,CAAA;AACd,IAAA,QAAA,CAAA,QAAA,CAAA,WAAA,CAAA,GAAA,EAAA,CAAA,GAAA,WAAS,CAAA;AACT,IAAA,QAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,GAAA,EAAA,CAAA,GAAA,iBAAe,CAAA;AACf,IAAA,QAAA,CAAA,QAAA,CAAA,YAAA,CAAA,GAAA,EAAA,CAAA,GAAA,YAAU,CAAA;AACV,IAAA,QAAA,CAAA,QAAA,CAAA,kBAAA,CAAA,GAAA,EAAA,CAAA,GAAA,kBAAgB,CAAA;AAClB,CAAC,EAhBW,QAAQ,KAAR,QAAQ,GAgBnB,EAAA,CAAA,CAAA,CAAA;AAED;;;;;AAKG;AACI,eAAe,UAAU,CAAC,EAAY,EAAA;IAC3C,MAAM,MAAM,CAAC,+BAA+B,EAAE;AAC5C,QAAA,QAAQ,EAAE,EAAE;AACb,KAAA,CAAC,CAAC;AACL;;;;"}
|
@ -0,0 +1,37 @@
|
||||
// Copyright 2021 Jonas Kruckenberg
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
|
||||
/**
|
||||
* Well known window positions.
|
||||
*/
|
||||
export enum Position {
|
||||
TopLeft = 0,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
TopCenter,
|
||||
BottomCenter,
|
||||
LeftCenter,
|
||||
RightCenter,
|
||||
Center,
|
||||
TrayLeft,
|
||||
TrayBottomLeft,
|
||||
TrayRight,
|
||||
TrayBottomRight,
|
||||
TrayCenter,
|
||||
TrayBottomCenter,
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the `Window` to the given {@link Position} using `WindowExt.move_window()`
|
||||
* All positions are relative to the **current** screen.
|
||||
*
|
||||
* @param to The {@link Position} to move to.
|
||||
*/
|
||||
export async function moveWindow(to: Position): Promise<void> {
|
||||
await invoke("plugin:positioner|move_window", {
|
||||
position: to,
|
||||
});
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "tauri-plugin-positioner-api",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"type": "module",
|
||||
"browser": "dist/index.min.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"browser": "./dist/index.min.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
import { createConfig } from "../../../shared/rollup.config.mjs";
|
||||
|
||||
export default createConfig({
|
||||
pkg: JSON.parse(
|
||||
readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
||||
),
|
||||
external: [/^@tauri-apps\/api/],
|
||||
});
|
@ -0,0 +1 @@
|
||||
../../../shared/tsconfig.json
|
@ -0,0 +1,182 @@
|
||||
// Copyright 2021 Jonas Kruckenberg
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[cfg(feature = "system-tray")]
|
||||
use crate::Tray;
|
||||
use serde_repr::Deserialize_repr;
|
||||
#[cfg(feature = "system-tray")]
|
||||
use tauri::Manager;
|
||||
use tauri::{PhysicalPosition, PhysicalSize, Result, Runtime, Window};
|
||||
|
||||
/// Well known window positions.
|
||||
#[derive(Debug, Deserialize_repr)]
|
||||
#[repr(u16)]
|
||||
pub enum Position {
|
||||
TopLeft = 0,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
TopCenter,
|
||||
BottomCenter,
|
||||
LeftCenter,
|
||||
RightCenter,
|
||||
Center,
|
||||
#[cfg(feature = "system-tray")]
|
||||
TrayLeft,
|
||||
#[cfg(feature = "system-tray")]
|
||||
TrayBottomLeft,
|
||||
#[cfg(feature = "system-tray")]
|
||||
TrayRight,
|
||||
#[cfg(feature = "system-tray")]
|
||||
TrayBottomRight,
|
||||
#[cfg(feature = "system-tray")]
|
||||
TrayCenter,
|
||||
#[cfg(feature = "system-tray")]
|
||||
TrayBottomCenter,
|
||||
}
|
||||
|
||||
/// A [`Window`] extension that provides extra methods related to positioning.
|
||||
pub trait WindowExt {
|
||||
/// Moves the [`Window`] to the given [`Position`]
|
||||
///
|
||||
/// All positions are relative to the **current** screen.
|
||||
fn move_window(&self, position: Position) -> Result<()>;
|
||||
}
|
||||
|
||||
impl<R: Runtime> WindowExt for Window<R> {
|
||||
fn move_window(&self, pos: Position) -> Result<()> {
|
||||
use Position::*;
|
||||
|
||||
let screen = self.current_monitor()?.unwrap();
|
||||
let screen_position = screen.position();
|
||||
let screen_size = PhysicalSize::<i32> {
|
||||
width: screen.size().width as i32,
|
||||
height: screen.size().height as i32,
|
||||
};
|
||||
let window_size = PhysicalSize::<i32> {
|
||||
width: self.outer_size()?.width as i32,
|
||||
height: self.outer_size()?.height as i32,
|
||||
};
|
||||
#[cfg(feature = "system-tray")]
|
||||
let (tray_position, tray_size) = self
|
||||
.state::<Tray>()
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.map(|(pos, size)| {
|
||||
(
|
||||
Some((pos.x as i32, pos.y as i32)),
|
||||
Some((size.width as i32, size.height as i32)),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let physical_pos = match pos {
|
||||
TopLeft => *screen_position,
|
||||
TopRight => PhysicalPosition {
|
||||
x: screen_position.x + (screen_size.width - window_size.width),
|
||||
y: screen_position.y,
|
||||
},
|
||||
BottomLeft => PhysicalPosition {
|
||||
x: screen_position.x,
|
||||
y: screen_size.height - (window_size.height - screen_position.y),
|
||||
},
|
||||
BottomRight => PhysicalPosition {
|
||||
x: screen_position.x + (screen_size.width - window_size.width),
|
||||
y: screen_size.height - (window_size.height - screen_position.y),
|
||||
},
|
||||
TopCenter => PhysicalPosition {
|
||||
x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)),
|
||||
y: screen_position.y,
|
||||
},
|
||||
BottomCenter => PhysicalPosition {
|
||||
x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)),
|
||||
y: screen_size.height - (window_size.height - screen_position.y),
|
||||
},
|
||||
LeftCenter => PhysicalPosition {
|
||||
x: screen_position.x,
|
||||
y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2),
|
||||
},
|
||||
RightCenter => PhysicalPosition {
|
||||
x: screen_position.x + (screen_size.width - window_size.width),
|
||||
y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2),
|
||||
},
|
||||
Center => PhysicalPosition {
|
||||
x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)),
|
||||
y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2),
|
||||
},
|
||||
#[cfg(feature = "system-tray")]
|
||||
TrayLeft => {
|
||||
if let Some((tray_x, tray_y)) = tray_position {
|
||||
PhysicalPosition {
|
||||
x: tray_x,
|
||||
y: tray_y - window_size.height,
|
||||
}
|
||||
} else {
|
||||
panic!("tray position not set");
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "system-tray")]
|
||||
TrayBottomLeft => {
|
||||
if let Some((tray_x, tray_y)) = tray_position {
|
||||
PhysicalPosition {
|
||||
x: tray_x,
|
||||
y: tray_y,
|
||||
}
|
||||
} else {
|
||||
panic!("Tray position not set");
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "system-tray")]
|
||||
TrayRight => {
|
||||
if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size)
|
||||
{
|
||||
PhysicalPosition {
|
||||
x: tray_x + tray_width,
|
||||
y: tray_y - window_size.height,
|
||||
}
|
||||
} else {
|
||||
panic!("Tray position not set");
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "system-tray")]
|
||||
TrayBottomRight => {
|
||||
if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size)
|
||||
{
|
||||
PhysicalPosition {
|
||||
x: tray_x + tray_width,
|
||||
y: tray_y,
|
||||
}
|
||||
} else {
|
||||
panic!("Tray position not set");
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "system-tray")]
|
||||
TrayCenter => {
|
||||
if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size)
|
||||
{
|
||||
PhysicalPosition {
|
||||
x: tray_x + (tray_width / 2) - (window_size.width / 2),
|
||||
y: tray_y - window_size.height,
|
||||
}
|
||||
} else {
|
||||
panic!("Tray position not set");
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "system-tray")]
|
||||
TrayBottomCenter => {
|
||||
if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size)
|
||||
{
|
||||
PhysicalPosition {
|
||||
x: tray_x + (tray_width / 2) - (window_size.width / 2),
|
||||
y: tray_y,
|
||||
}
|
||||
} else {
|
||||
panic!("Tray position not set");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.set_position(tauri::Position::Physical(physical_pos))
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
// Copyright 2021 Jonas Kruckenberg
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! A plugin for Tauri that helps position your windows at well-known locations.
|
||||
//!
|
||||
//! # Cargo features
|
||||
//!
|
||||
//! - **system-tray**: Enables system-tray-relative positions.
|
||||
//!
|
||||
//! Note: This requires attaching the Tauri plugin, *even* when using the trait extension only.
|
||||
|
||||
mod ext;
|
||||
|
||||
pub use ext::*;
|
||||
use tauri::{
|
||||
plugin::{self, TauriPlugin},
|
||||
Result, Runtime,
|
||||
};
|
||||
|
||||
#[cfg(feature = "system-tray")]
|
||||
use tauri::{AppHandle, Manager, PhysicalPosition, PhysicalSize, SystemTrayEvent};
|
||||
|
||||
#[cfg(feature = "system-tray")]
|
||||
struct Tray(std::sync::Mutex<Option<(PhysicalPosition<f64>, PhysicalSize<f64>)>>);
|
||||
|
||||
#[cfg(feature = "system-tray")]
|
||||
pub fn on_tray_event<R: Runtime>(app: &AppHandle<R>, event: &SystemTrayEvent) {
|
||||
match event {
|
||||
SystemTrayEvent::LeftClick { position, size, .. } => {
|
||||
app.state::<Tray>()
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace((*position, *size));
|
||||
}
|
||||
SystemTrayEvent::RightClick { position, size, .. } => {
|
||||
app.state::<Tray>()
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace((*position, *size));
|
||||
}
|
||||
SystemTrayEvent::DoubleClick { position, size, .. } => {
|
||||
app.state::<Tray>()
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace((*position, *size));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn move_window<R: Runtime>(window: tauri::Window<R>, position: Position) -> Result<()> {
|
||||
window.move_window(position)
|
||||
}
|
||||
|
||||
/// The Tauri plugin that exposes [`WindowExt::move_window`] to the webview.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
let plugin =
|
||||
plugin::Builder::new("positioner").invoke_handler(tauri::generate_handler![move_window]);
|
||||
|
||||
#[cfg(feature = "system-tray")]
|
||||
let plugin = plugin.setup(|app_handle| {
|
||||
app_handle.manage(Tray(std::sync::Mutex::new(None)));
|
||||
Ok(())
|
||||
});
|
||||
|
||||
plugin.build()
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "tauri-plugin-sql"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "json"] }
|
||||
tokio = { version = "1", features = ["sync"] }
|
||||
futures = "0.3"
|
||||
|
||||
[features]
|
||||
sqlite = ["sqlx/sqlite"]
|
||||
mysql = ["sqlx/mysql"]
|
||||
postgres = ["sqlx/postgres"]
|
@ -0,0 +1,119 @@
|
||||
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0});};
|
||||
|
||||
var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}
|
||||
|
||||
/**
|
||||
* **Database**
|
||||
*
|
||||
* The `Database` class serves as the primary interface for
|
||||
* communicating with the rust side of the sql plugin.
|
||||
*/
|
||||
class Database {
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
}
|
||||
/**
|
||||
* **load**
|
||||
*
|
||||
* A static initializer which connects to the underlying database and
|
||||
* returns a `Database` instance once a connection to the database is established.
|
||||
*
|
||||
* # Sqlite
|
||||
*
|
||||
* The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const db = await Database.load("sqlite:test.db");
|
||||
* ```
|
||||
*/
|
||||
static async load(path) {
|
||||
const _path = await c("plugin:sql|load", {
|
||||
db: path,
|
||||
});
|
||||
return new Database(_path);
|
||||
}
|
||||
/**
|
||||
* **get**
|
||||
*
|
||||
* A static initializer which synchronously returns an instance of
|
||||
* the Database class while deferring the actual database connection
|
||||
* until the first invocation or selection on the database.
|
||||
*
|
||||
* # Sqlite
|
||||
*
|
||||
* The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const db = Database.get("sqlite:test.db");
|
||||
* ```
|
||||
*/
|
||||
static get(path) {
|
||||
return new Database(path);
|
||||
}
|
||||
/**
|
||||
* **execute**
|
||||
*
|
||||
* Passes a SQL expression to the database for execution.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = await db.execute(
|
||||
* "UPDATE todos SET title = $1, completed = $2 WHERE id = $3",
|
||||
* [ todos.title, todos.status, todos.id ]
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async execute(query, bindValues) {
|
||||
const [rowsAffected, lastInsertId] = await c("plugin:sql|execute", {
|
||||
db: this.path,
|
||||
query,
|
||||
values: bindValues !== null && bindValues !== void 0 ? bindValues : [],
|
||||
});
|
||||
return {
|
||||
lastInsertId,
|
||||
rowsAffected,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* **select**
|
||||
*
|
||||
* Passes in a SELECT query to the database for execution.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = await db.select(
|
||||
* "SELECT * from todos WHERE id = $1", id
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async select(query, bindValues) {
|
||||
const result = await c("plugin:sql|select", {
|
||||
db: this.path,
|
||||
query,
|
||||
values: bindValues !== null && bindValues !== void 0 ? bindValues : [],
|
||||
});
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* **close**
|
||||
*
|
||||
* Closes the database connection pool.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const success = await db.close()
|
||||
* ```
|
||||
* @param db - Optionally state the name of a database if you are managing more than one. Otherwise, all database pools will be in scope.
|
||||
*/
|
||||
async close(db) {
|
||||
const success = await c("plugin:sql|close", {
|
||||
db,
|
||||
});
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
export { Database as default };
|
||||
//# sourceMappingURL=index.min.js.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.min.js","sources":["../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-FEIY7W7S.js","../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-RCPA6UVN.js","../index.ts"],"sourcesContent":["var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0})};export{e as a};\n","import{a as d}from\"./chunk-FEIY7W7S.js\";var f={};d(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`)},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`)},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r})})}function w(e,r=\"asset\"){let n=encodeURIComponent(e);return navigator.userAgent.includes(\"Windows\")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}export{s as a,c as b,w as c,f as d};\n",null],"names":["d","invoke"],"mappings":"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"}
|
@ -0,0 +1,117 @@
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
|
||||
/**
|
||||
* **Database**
|
||||
*
|
||||
* The `Database` class serves as the primary interface for
|
||||
* communicating with the rust side of the sql plugin.
|
||||
*/
|
||||
class Database {
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
}
|
||||
/**
|
||||
* **load**
|
||||
*
|
||||
* A static initializer which connects to the underlying database and
|
||||
* returns a `Database` instance once a connection to the database is established.
|
||||
*
|
||||
* # Sqlite
|
||||
*
|
||||
* The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const db = await Database.load("sqlite:test.db");
|
||||
* ```
|
||||
*/
|
||||
static async load(path) {
|
||||
const _path = await invoke("plugin:sql|load", {
|
||||
db: path,
|
||||
});
|
||||
return new Database(_path);
|
||||
}
|
||||
/**
|
||||
* **get**
|
||||
*
|
||||
* A static initializer which synchronously returns an instance of
|
||||
* the Database class while deferring the actual database connection
|
||||
* until the first invocation or selection on the database.
|
||||
*
|
||||
* # Sqlite
|
||||
*
|
||||
* The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const db = Database.get("sqlite:test.db");
|
||||
* ```
|
||||
*/
|
||||
static get(path) {
|
||||
return new Database(path);
|
||||
}
|
||||
/**
|
||||
* **execute**
|
||||
*
|
||||
* Passes a SQL expression to the database for execution.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = await db.execute(
|
||||
* "UPDATE todos SET title = $1, completed = $2 WHERE id = $3",
|
||||
* [ todos.title, todos.status, todos.id ]
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async execute(query, bindValues) {
|
||||
const [rowsAffected, lastInsertId] = await invoke("plugin:sql|execute", {
|
||||
db: this.path,
|
||||
query,
|
||||
values: bindValues !== null && bindValues !== void 0 ? bindValues : [],
|
||||
});
|
||||
return {
|
||||
lastInsertId,
|
||||
rowsAffected,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* **select**
|
||||
*
|
||||
* Passes in a SELECT query to the database for execution.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = await db.select(
|
||||
* "SELECT * from todos WHERE id = $1", id
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async select(query, bindValues) {
|
||||
const result = await invoke("plugin:sql|select", {
|
||||
db: this.path,
|
||||
query,
|
||||
values: bindValues !== null && bindValues !== void 0 ? bindValues : [],
|
||||
});
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* **close**
|
||||
*
|
||||
* Closes the database connection pool.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const success = await db.close()
|
||||
* ```
|
||||
* @param db - Optionally state the name of a database if you are managing more than one. Otherwise, all database pools will be in scope.
|
||||
*/
|
||||
async close(db) {
|
||||
const success = await invoke("plugin:sql|close", {
|
||||
db,
|
||||
});
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
export { Database as default };
|
||||
//# sourceMappingURL=index.mjs.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAgBA;;;;;AAKG;AACW,MAAO,QAAQ,CAAA;AAE3B,IAAA,WAAA,CAAY,IAAY,EAAA;AACtB,QAAA,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;KAClB;AAED;;;;;;;;;;;;;;AAcG;AACH,IAAA,aAAa,IAAI,CAAC,IAAY,EAAA;AAC5B,QAAA,MAAM,KAAK,GAAG,MAAM,MAAM,CAAS,iBAAiB,EAAE;AACpD,YAAA,EAAE,EAAE,IAAI;AACT,SAAA,CAAC,CAAC;AAEH,QAAA,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC;KAC5B;AAED;;;;;;;;;;;;;;;AAeG;IACH,OAAO,GAAG,CAAC,IAAY,EAAA;AACrB,QAAA,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;KAC3B;AAED;;;;;;;;;;;;AAYG;AACH,IAAA,MAAM,OAAO,CAAC,KAAa,EAAE,UAAsB,EAAA;QACjD,MAAM,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,MAAM,MAAM,CAC/C,oBAAoB,EACpB;YACE,EAAE,EAAE,IAAI,CAAC,IAAI;YACb,KAAK;AACL,YAAA,MAAM,EAAE,UAAU,KAAA,IAAA,IAAV,UAAU,KAAV,KAAA,CAAA,GAAA,UAAU,GAAI,EAAE;AACzB,SAAA,CACF,CAAC;QAEF,OAAO;YACL,YAAY;YACZ,YAAY;SACb,CAAC;KACH;AAED;;;;;;;;;;;AAWG;AACH,IAAA,MAAM,MAAM,CAAI,KAAa,EAAE,UAAsB,EAAA;AACnD,QAAA,MAAM,MAAM,GAAG,MAAM,MAAM,CAAI,mBAAmB,EAAE;YAClD,EAAE,EAAE,IAAI,CAAC,IAAI;YACb,KAAK;AACL,YAAA,MAAM,EAAE,UAAU,KAAA,IAAA,IAAV,UAAU,KAAV,KAAA,CAAA,GAAA,UAAU,GAAI,EAAE;AACzB,SAAA,CAAC,CAAC;AAEH,QAAA,OAAO,MAAM,CAAC;KACf;AAED;;;;;;;;;;AAUG;IACH,MAAM,KAAK,CAAC,EAAW,EAAA;AACrB,QAAA,MAAM,OAAO,GAAG,MAAM,MAAM,CAAU,kBAAkB,EAAE;YACxD,EAAE;AACH,SAAA,CAAC,CAAC;AACH,QAAA,OAAO,OAAO,CAAC;KAChB;AACF;;;;"}
|
@ -0,0 +1,140 @@
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
|
||||
export interface QueryResult {
|
||||
/** The number of rows affected by the query. */
|
||||
rowsAffected: number;
|
||||
/**
|
||||
* The last inserted `id`.
|
||||
*
|
||||
* This value is always `0` when using the Postgres driver. If the
|
||||
* last inserted id is required on Postgres, the `select` function
|
||||
* must be used, with a `RETURNING` clause
|
||||
* (`INSERT INTO todos (title) VALUES ($1) RETURNING id`).
|
||||
*/
|
||||
lastInsertId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* **Database**
|
||||
*
|
||||
* The `Database` class serves as the primary interface for
|
||||
* communicating with the rust side of the sql plugin.
|
||||
*/
|
||||
export default class Database {
|
||||
path: string;
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* **load**
|
||||
*
|
||||
* A static initializer which connects to the underlying database and
|
||||
* returns a `Database` instance once a connection to the database is established.
|
||||
*
|
||||
* # Sqlite
|
||||
*
|
||||
* The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const db = await Database.load("sqlite:test.db");
|
||||
* ```
|
||||
*/
|
||||
static async load(path: string): Promise<Database> {
|
||||
const _path = await invoke<string>("plugin:sql|load", {
|
||||
db: path,
|
||||
});
|
||||
|
||||
return new Database(_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* **get**
|
||||
*
|
||||
* A static initializer which synchronously returns an instance of
|
||||
* the Database class while deferring the actual database connection
|
||||
* until the first invocation or selection on the database.
|
||||
*
|
||||
* # Sqlite
|
||||
*
|
||||
* The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const db = Database.get("sqlite:test.db");
|
||||
* ```
|
||||
*/
|
||||
static get(path: string): Database {
|
||||
return new Database(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* **execute**
|
||||
*
|
||||
* Passes a SQL expression to the database for execution.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = await db.execute(
|
||||
* "UPDATE todos SET title = $1, completed = $2 WHERE id = $3",
|
||||
* [ todos.title, todos.status, todos.id ]
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async execute(query: string, bindValues?: unknown[]): Promise<QueryResult> {
|
||||
const [rowsAffected, lastInsertId] = await invoke<[number, number]>(
|
||||
"plugin:sql|execute",
|
||||
{
|
||||
db: this.path,
|
||||
query,
|
||||
values: bindValues ?? [],
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
lastInsertId,
|
||||
rowsAffected,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* **select**
|
||||
*
|
||||
* Passes in a SELECT query to the database for execution.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = await db.select(
|
||||
* "SELECT * from todos WHERE id = $1", id
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async select<T>(query: string, bindValues?: unknown[]): Promise<T> {
|
||||
const result = await invoke<T>("plugin:sql|select", {
|
||||
db: this.path,
|
||||
query,
|
||||
values: bindValues ?? [],
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* **close**
|
||||
*
|
||||
* Closes the database connection pool.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const success = await db.close()
|
||||
* ```
|
||||
* @param db - Optionally state the name of a database if you are managing more than one. Otherwise, all database pools will be in scope.
|
||||
*/
|
||||
async close(db?: string): Promise<boolean> {
|
||||
const success = await invoke<boolean>("plugin:sql|close", {
|
||||
db,
|
||||
});
|
||||
return success;
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "tauri-plugin-{{name}}-api",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"type": "module",
|
||||
"browser": "dist/index.min.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"browser": "./dist/index.min.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
import { createConfig } from "../../../shared/rollup.config.mjs";
|
||||
|
||||
export default createConfig({
|
||||
pkg: JSON.parse(
|
||||
readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
||||
),
|
||||
external: [/^@tauri-apps\/api/],
|
||||
});
|
@ -0,0 +1 @@
|
||||
../../../shared/tsconfig.json
|
@ -0,0 +1,28 @@
|
||||
// Copyright 2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[cfg(any(
|
||||
all(feature = "sqlite", feature = "mysql"),
|
||||
all(feature = "sqlite", feature = "postgres"),
|
||||
all(feature = "mysql", feature = "postgres")
|
||||
))]
|
||||
compile_error!("Only one database driver can be enabled. Use `default-features = false` and set the feature flag for the driver of your choice.");
|
||||
|
||||
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
|
||||
compile_error!(
|
||||
"Database driver not defined. Please set the feature flag for the driver of your choice."
|
||||
);
|
||||
|
||||
#[cfg(any(
|
||||
all(feature = "sqlite", not(any(feature = "mysql", feature = "postgres"))),
|
||||
all(feature = "mysql", not(any(feature = "sqlite", feature = "postgres"))),
|
||||
all(feature = "postgres", not(any(feature = "sqlite", feature = "mysql"))),
|
||||
))]
|
||||
mod plugin;
|
||||
#[cfg(any(
|
||||
all(feature = "sqlite", not(any(feature = "mysql", feature = "postgres"))),
|
||||
all(feature = "mysql", not(any(feature = "sqlite", feature = "postgres"))),
|
||||
all(feature = "postgres", not(any(feature = "sqlite", feature = "mysql"))),
|
||||
))]
|
||||
pub use plugin::*;
|
@ -0,0 +1,387 @@
|
||||
// Copyright 2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use futures::future::BoxFuture;
|
||||
use serde::{ser::Serializer, Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use sqlx::{
|
||||
error::BoxDynError,
|
||||
migrate::{
|
||||
MigrateDatabase, Migration as SqlxMigration, MigrationSource, MigrationType, Migrator,
|
||||
},
|
||||
Column, Pool, Row, TypeInfo,
|
||||
};
|
||||
use tauri::{
|
||||
command,
|
||||
plugin::{Plugin, Result as PluginResult},
|
||||
AppHandle, Invoke, Manager, RunEvent, Runtime, State,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use std::{fs::create_dir_all, path::PathBuf};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
type Db = sqlx::sqlite::Sqlite;
|
||||
#[cfg(feature = "mysql")]
|
||||
type Db = sqlx::mysql::MySql;
|
||||
#[cfg(feature = "postgres")]
|
||||
type Db = sqlx::postgres::Postgres;
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
type LastInsertId = i64;
|
||||
#[cfg(not(feature = "sqlite"))]
|
||||
type LastInsertId = u64;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Sql(#[from] sqlx::Error),
|
||||
#[error(transparent)]
|
||||
Migration(#[from] sqlx::migrate::MigrateError),
|
||||
#[error("database {0} not loaded")]
|
||||
DatabaseNotLoaded(String),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
/// Resolves the App's **file path** from the `AppHandle` context
|
||||
/// object
|
||||
fn app_path<R: Runtime>(app: &AppHandle<R>) -> PathBuf {
|
||||
#[allow(deprecated)] // FIXME: Change to non-deprecated function in Tauri v2
|
||||
app.path_resolver()
|
||||
.app_dir()
|
||||
.expect("No App path was found!")
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
/// Maps the user supplied DB connection string to a connection string
|
||||
/// with a fully qualified file path to the App's designed "app_path"
|
||||
fn path_mapper(mut app_path: PathBuf, connection_string: &str) -> String {
|
||||
app_path.push(
|
||||
connection_string
|
||||
.split_once(':')
|
||||
.expect("Couldn't parse the connection string for DB!")
|
||||
.1,
|
||||
);
|
||||
|
||||
format!(
|
||||
"sqlite:{}",
|
||||
app_path
|
||||
.to_str()
|
||||
.expect("Problem creating fully qualified path to Database file!")
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DbInstances(Mutex<HashMap<String, Pool<Db>>>);
|
||||
|
||||
struct Migrations(Mutex<HashMap<String, MigrationList>>);
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
struct PluginConfig {
|
||||
#[serde(default)]
|
||||
preload: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MigrationKind {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
impl From<MigrationKind> for MigrationType {
|
||||
fn from(kind: MigrationKind) -> Self {
|
||||
match kind {
|
||||
MigrationKind::Up => Self::ReversibleUp,
|
||||
MigrationKind::Down => Self::ReversibleDown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A migration definition.
|
||||
#[derive(Debug)]
|
||||
pub struct Migration {
|
||||
pub version: i64,
|
||||
pub description: &'static str,
|
||||
pub sql: &'static str,
|
||||
pub kind: MigrationKind,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MigrationList(Vec<Migration>);
|
||||
|
||||
impl MigrationSource<'static> for MigrationList {
|
||||
fn resolve(self) -> BoxFuture<'static, std::result::Result<Vec<SqlxMigration>, BoxDynError>> {
|
||||
Box::pin(async move {
|
||||
let mut migrations = Vec::new();
|
||||
for migration in self.0 {
|
||||
if matches!(migration.kind, MigrationKind::Up) {
|
||||
migrations.push(SqlxMigration::new(
|
||||
migration.version,
|
||||
migration.description.into(),
|
||||
migration.kind.into(),
|
||||
migration.sql.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(migrations)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn load<R: Runtime>(
|
||||
#[allow(unused_variables)] app: AppHandle<R>,
|
||||
db_instances: State<'_, DbInstances>,
|
||||
migrations: State<'_, Migrations>,
|
||||
db: String,
|
||||
) -> Result<String> {
|
||||
#[cfg(feature = "sqlite")]
|
||||
let fqdb = path_mapper(app_path(&app), &db);
|
||||
#[cfg(not(feature = "sqlite"))]
|
||||
let fqdb = db.clone();
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
create_dir_all(app_path(&app)).expect("Problem creating App directory!");
|
||||
|
||||
if !Db::database_exists(&fqdb).await.unwrap_or(false) {
|
||||
Db::create_database(&fqdb).await?;
|
||||
}
|
||||
let pool = Pool::connect(&fqdb).await?;
|
||||
|
||||
if let Some(migrations) = migrations.0.lock().await.remove(&db) {
|
||||
let migrator = Migrator::new(migrations).await?;
|
||||
migrator.run(&pool).await?;
|
||||
}
|
||||
|
||||
db_instances.0.lock().await.insert(db.clone(), pool);
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
/// Allows the database connection(s) to be closed; if no database
|
||||
/// name is passed in then _all_ database connection pools will be
|
||||
/// shut down.
|
||||
#[command]
|
||||
async fn close(db_instances: State<'_, DbInstances>, db: Option<String>) -> Result<bool> {
|
||||
let mut instances = db_instances.0.lock().await;
|
||||
|
||||
let pools = if let Some(db) = db {
|
||||
vec![db]
|
||||
} else {
|
||||
instances.keys().cloned().collect()
|
||||
};
|
||||
|
||||
for pool in pools {
|
||||
let db = instances
|
||||
.get_mut(&pool) //
|
||||
.ok_or(Error::DatabaseNotLoaded(pool))?;
|
||||
db.close().await;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Execute a command against the database
|
||||
#[command]
|
||||
async fn execute(
|
||||
db_instances: State<'_, DbInstances>,
|
||||
db: String,
|
||||
query: String,
|
||||
values: Vec<JsonValue>,
|
||||
) -> Result<(u64, LastInsertId)> {
|
||||
let mut instances = db_instances.0.lock().await;
|
||||
|
||||
let db = instances.get_mut(&db).ok_or(Error::DatabaseNotLoaded(db))?;
|
||||
let mut query = sqlx::query(&query);
|
||||
for value in values {
|
||||
if value.is_string() {
|
||||
query = query.bind(value.as_str().unwrap().to_owned())
|
||||
} else {
|
||||
query = query.bind(value);
|
||||
}
|
||||
}
|
||||
let result = query.execute(&*db).await?;
|
||||
#[cfg(feature = "sqlite")]
|
||||
let r = Ok((result.rows_affected(), result.last_insert_rowid()));
|
||||
#[cfg(feature = "mysql")]
|
||||
let r = Ok((result.rows_affected(), result.last_insert_id()));
|
||||
#[cfg(feature = "postgres")]
|
||||
let r = Ok((result.rows_affected(), 0));
|
||||
r
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn select(
|
||||
db_instances: State<'_, DbInstances>,
|
||||
db: String,
|
||||
query: String,
|
||||
values: Vec<JsonValue>,
|
||||
) -> Result<Vec<HashMap<String, JsonValue>>> {
|
||||
let mut instances = db_instances.0.lock().await;
|
||||
let db = instances.get_mut(&db).ok_or(Error::DatabaseNotLoaded(db))?;
|
||||
let mut query = sqlx::query(&query);
|
||||
for value in values {
|
||||
if value.is_string() {
|
||||
query = query.bind(value.as_str().unwrap().to_owned())
|
||||
} else {
|
||||
query = query.bind(value);
|
||||
}
|
||||
}
|
||||
let rows = query.fetch_all(&*db).await?;
|
||||
let mut values = Vec::new();
|
||||
for row in rows {
|
||||
let mut value = HashMap::default();
|
||||
for (i, column) in row.columns().iter().enumerate() {
|
||||
let info = column.type_info();
|
||||
let v = if info.is_null() {
|
||||
JsonValue::Null
|
||||
} else {
|
||||
match info.name() {
|
||||
"VARCHAR" | "STRING" | "TEXT" | "DATETIME" => {
|
||||
if let Ok(s) = row.try_get(i) {
|
||||
JsonValue::String(s)
|
||||
} else {
|
||||
JsonValue::Null
|
||||
}
|
||||
}
|
||||
"BOOL" | "BOOLEAN" => {
|
||||
if let Ok(b) = row.try_get(i) {
|
||||
JsonValue::Bool(b)
|
||||
} else {
|
||||
let x: String = row.get(i);
|
||||
JsonValue::Bool(x.to_lowercase() == "true")
|
||||
}
|
||||
}
|
||||
"INT" | "NUMBER" | "INTEGER" | "BIGINT" | "INT8" => {
|
||||
if let Ok(n) = row.try_get::<i64, usize>(i) {
|
||||
JsonValue::Number(n.into())
|
||||
} else {
|
||||
JsonValue::Null
|
||||
}
|
||||
}
|
||||
"REAL" => {
|
||||
if let Ok(n) = row.try_get::<f64, usize>(i) {
|
||||
JsonValue::from(n)
|
||||
} else {
|
||||
JsonValue::Null
|
||||
}
|
||||
}
|
||||
// "JSON" => JsonValue::Object(row.get(i)),
|
||||
"BLOB" => {
|
||||
if let Ok(n) = row.try_get::<Vec<u8>, usize>(i) {
|
||||
JsonValue::Array(
|
||||
n.into_iter().map(|n| JsonValue::Number(n.into())).collect(),
|
||||
)
|
||||
} else {
|
||||
JsonValue::Null
|
||||
}
|
||||
}
|
||||
_ => JsonValue::Null,
|
||||
}
|
||||
};
|
||||
value.insert(column.name().to_string(), v);
|
||||
}
|
||||
values.push(value);
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
/// Tauri SQL plugin.
|
||||
pub struct TauriSql<R: Runtime> {
|
||||
migrations: Option<HashMap<String, MigrationList>>,
|
||||
invoke_handler: Box<dyn Fn(Invoke<R>) + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Default for TauriSql<R> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
migrations: Some(Default::default()),
|
||||
invoke_handler: Box::new(tauri::generate_handler![load, execute, select, close]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> TauriSql<R> {
|
||||
/// Add migrations to a database.
|
||||
#[must_use]
|
||||
pub fn add_migrations(mut self, db_url: &str, migrations: Vec<Migration>) -> Self {
|
||||
self.migrations
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.insert(db_url.to_string(), MigrationList(migrations));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Plugin<R> for TauriSql<R> {
|
||||
fn name(&self) -> &'static str {
|
||||
"sql"
|
||||
}
|
||||
|
||||
fn initialize(&mut self, app: &AppHandle<R>, config: serde_json::Value) -> PluginResult<()> {
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let config: PluginConfig = if config.is_null() {
|
||||
Default::default()
|
||||
} else {
|
||||
serde_json::from_value(config)?
|
||||
};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
create_dir_all(app_path(app)).expect("problems creating App directory!");
|
||||
|
||||
let instances = DbInstances::default();
|
||||
let mut lock = instances.0.lock().await;
|
||||
for db in config.preload {
|
||||
#[cfg(feature = "sqlite")]
|
||||
let fqdb = path_mapper(app_path(app), &db);
|
||||
#[cfg(not(feature = "sqlite"))]
|
||||
let fqdb = db.clone();
|
||||
|
||||
if !Db::database_exists(&fqdb).await.unwrap_or(false) {
|
||||
Db::create_database(&fqdb).await?;
|
||||
}
|
||||
let pool = Pool::connect(&fqdb).await?;
|
||||
|
||||
if let Some(migrations) = self.migrations.as_mut().unwrap().remove(&db) {
|
||||
let migrator = Migrator::new(migrations).await?;
|
||||
migrator.run(&pool).await?;
|
||||
}
|
||||
lock.insert(db, pool);
|
||||
}
|
||||
drop(lock);
|
||||
app.manage(instances);
|
||||
app.manage(Migrations(Mutex::new(self.migrations.take().unwrap())));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn extend_api(&mut self, message: Invoke<R>) {
|
||||
(self.invoke_handler)(message)
|
||||
}
|
||||
|
||||
fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
|
||||
if let RunEvent::Exit = event {
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let instances = &*app.state::<DbInstances>();
|
||||
let instances = instances.0.lock().await;
|
||||
for value in instances.values() {
|
||||
value.close().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "tauri-plugin-store"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,177 @@
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
|
||||
// Copyright 2021 Tauri Programme within The Commons Conservancy
|
||||
/**
|
||||
* A key-value store persisted by the backend layer.
|
||||
*/
|
||||
class Store {
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
}
|
||||
/**
|
||||
* Inserts a key-value pair into the store.
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
async set(key, value) {
|
||||
await invoke("plugin:store|set", {
|
||||
path: this.path,
|
||||
key,
|
||||
value,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Returns the value for the given `key` or `null` the key does not exist.
|
||||
*
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
async get(key) {
|
||||
return await invoke("plugin:store|get", {
|
||||
path: this.path,
|
||||
key,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Returns `true` if the given `key` exists in the store.
|
||||
*
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
async has(key) {
|
||||
return await invoke("plugin:store|has", {
|
||||
path: this.path,
|
||||
key,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Removes a key-value pair from the store.
|
||||
*
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
async delete(key) {
|
||||
return await invoke("plugin:store|delete", {
|
||||
path: this.path,
|
||||
key,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Clears the store, removing all key-value pairs.
|
||||
*
|
||||
* Note: To clear the storage and reset it to it's `default` value, use `reset` instead.
|
||||
* @returns
|
||||
*/
|
||||
async clear() {
|
||||
return await invoke("plugin:store|clear", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Resets the store to it's `default` value.
|
||||
*
|
||||
* If no default value has been set, this method behaves identical to `clear`.
|
||||
* @returns
|
||||
*/
|
||||
async reset() {
|
||||
return await invoke("plugin:store|reset", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Returns a list of all key in the store.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
async keys() {
|
||||
return await invoke("plugin:store|keys", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Returns a list of all values in the store.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
async values() {
|
||||
return await invoke("plugin:store|values", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Returns a list of all entries in the store.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
async entries() {
|
||||
return await invoke("plugin:store|entries", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Returns the number of key-value pairs in the store.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
async length() {
|
||||
return await invoke("plugin:store|length", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Attempts to load the on-disk state at the stores `path` into memory.
|
||||
*
|
||||
* This method is useful if the on-disk state was edited by the user and you want to synchronize the changes.
|
||||
*
|
||||
* Note: This method does not emit change events.
|
||||
* @returns
|
||||
*/
|
||||
async load() {
|
||||
return await invoke("plugin:store|load", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Saves the store to disk at the stores `path`.
|
||||
*
|
||||
* As the store is only persistet to disk before the apps exit, changes might be lost in a crash.
|
||||
* This method let's you persist the store to disk whenever you deem necessary.
|
||||
* @returns
|
||||
*/
|
||||
async save() {
|
||||
return await invoke("plugin:store|save", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Listen to changes on a store key.
|
||||
* @param key
|
||||
* @param cb
|
||||
* @returns A promise resolving to a function to unlisten to the event.
|
||||
*/
|
||||
async onKeyChange(key, cb) {
|
||||
return await appWindow.listen("store://change", (event) => {
|
||||
if (event.payload.path === this.path && event.payload.key === key) {
|
||||
cb(event.payload.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Listen to changes on the store.
|
||||
* @param cb
|
||||
* @returns A promise resolving to a function to unlisten to the event.
|
||||
*/
|
||||
async onChange(cb) {
|
||||
return await appWindow.listen("store://change", (event) => {
|
||||
if (event.payload.path === this.path) {
|
||||
cb(event.payload.key, event.payload.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { Store };
|
||||
//# sourceMappingURL=index.mjs.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
@ -0,0 +1,210 @@
|
||||
// Copyright 2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
|
||||
interface ChangePayload<T> {
|
||||
path: string;
|
||||
key: string;
|
||||
value: T | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A key-value store persisted by the backend layer.
|
||||
*/
|
||||
export class Store {
|
||||
path: string;
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a key-value pair into the store.
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
async set(key: string, value: unknown): Promise<void> {
|
||||
await invoke("plugin:store|set", {
|
||||
path: this.path,
|
||||
key,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the given `key` or `null` the key does not exist.
|
||||
*
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
return await invoke("plugin:store|get", {
|
||||
path: this.path,
|
||||
key,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the given `key` exists in the store.
|
||||
*
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
async has(key: string): Promise<boolean> {
|
||||
return await invoke("plugin:store|has", {
|
||||
path: this.path,
|
||||
key,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a key-value pair from the store.
|
||||
*
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
async delete(key: string): Promise<boolean> {
|
||||
return await invoke("plugin:store|delete", {
|
||||
path: this.path,
|
||||
key,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the store, removing all key-value pairs.
|
||||
*
|
||||
* Note: To clear the storage and reset it to it's `default` value, use `reset` instead.
|
||||
* @returns
|
||||
*/
|
||||
async clear(): Promise<void> {
|
||||
return await invoke("plugin:store|clear", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the store to it's `default` value.
|
||||
*
|
||||
* If no default value has been set, this method behaves identical to `clear`.
|
||||
* @returns
|
||||
*/
|
||||
async reset(): Promise<void> {
|
||||
return await invoke("plugin:store|reset", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all key in the store.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
async keys(): Promise<string[]> {
|
||||
return await invoke("plugin:store|keys", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all values in the store.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
async values(): Promise<string[]> {
|
||||
return await invoke("plugin:store|values", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all entries in the store.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
async entries<T>(): Promise<Array<[key: string, value: T]>> {
|
||||
return await invoke("plugin:store|entries", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of key-value pairs in the store.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
async length(): Promise<string[]> {
|
||||
return await invoke("plugin:store|length", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to load the on-disk state at the stores `path` into memory.
|
||||
*
|
||||
* This method is useful if the on-disk state was edited by the user and you want to synchronize the changes.
|
||||
*
|
||||
* Note: This method does not emit change events.
|
||||
* @returns
|
||||
*/
|
||||
async load(): Promise<void> {
|
||||
return await invoke("plugin:store|load", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the store to disk at the stores `path`.
|
||||
*
|
||||
* As the store is only persistet to disk before the apps exit, changes might be lost in a crash.
|
||||
* This method let's you persist the store to disk whenever you deem necessary.
|
||||
* @returns
|
||||
*/
|
||||
async save(): Promise<void> {
|
||||
return await invoke("plugin:store|save", {
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to changes on a store key.
|
||||
* @param key
|
||||
* @param cb
|
||||
* @returns A promise resolving to a function to unlisten to the event.
|
||||
*/
|
||||
async onKeyChange<T>(
|
||||
key: string,
|
||||
cb: (value: T | null) => void
|
||||
): Promise<UnlistenFn> {
|
||||
return await appWindow.listen<ChangePayload<T>>(
|
||||
"store://change",
|
||||
(event) => {
|
||||
if (event.payload.path === this.path && event.payload.key === key) {
|
||||
cb(event.payload.value);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to changes on the store.
|
||||
* @param cb
|
||||
* @returns A promise resolving to a function to unlisten to the event.
|
||||
*/
|
||||
async onChange(
|
||||
cb: (key: string, value: unknown) => void
|
||||
): Promise<UnlistenFn> {
|
||||
return await appWindow.listen<ChangePayload<unknown>>(
|
||||
"store://change",
|
||||
(event) => {
|
||||
if (event.payload.path === this.path) {
|
||||
cb(event.payload.key, event.payload.value);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "tauri-plugin-store-api",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"type": "module",
|
||||
"browser": "dist/index.min.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"browser": "./dist/index.min.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
import { createConfig } from "../../../shared/rollup.config.mjs";
|
||||
|
||||
export default createConfig({
|
||||
pkg: JSON.parse(
|
||||
readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
||||
),
|
||||
external: [/^@tauri-apps\/api/],
|
||||
});
|
@ -0,0 +1 @@
|
||||
../../../shared/tsconfig.json
|
@ -0,0 +1,34 @@
|
||||
// Copyright 2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// The error types.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[error("Failed to serialize store. {0}")]
|
||||
Serialize(Box<dyn std::error::Error>),
|
||||
#[error("Failed to deserialize store. {0}")]
|
||||
Deserialize(Box<dyn std::error::Error>),
|
||||
/// JSON error.
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
/// IO error.
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
/// Store not found
|
||||
#[error("Store \"{0}\" not found")]
|
||||
NotFound(PathBuf),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
@ -0,0 +1,370 @@
|
||||
// Copyright 2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pub use error::Error;
|
||||
use log::warn;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
|
||||
pub use store::{Store, StoreBuilder};
|
||||
use tauri::{
|
||||
plugin::{self, TauriPlugin},
|
||||
AppHandle, Manager, RunEvent, Runtime, State, Window,
|
||||
};
|
||||
|
||||
mod error;
|
||||
mod store;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
struct ChangePayload {
|
||||
path: PathBuf,
|
||||
key: String,
|
||||
value: JsonValue,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StoreCollection {
|
||||
stores: Mutex<HashMap<PathBuf, Store>>,
|
||||
frozen: bool,
|
||||
}
|
||||
|
||||
fn with_store<R: Runtime, T, F: FnOnce(&mut Store) -> Result<T, Error>>(
|
||||
app: &AppHandle<R>,
|
||||
collection: State<'_, StoreCollection>,
|
||||
path: PathBuf,
|
||||
f: F,
|
||||
) -> Result<T, Error> {
|
||||
let mut stores = collection.stores.lock().expect("mutex poisoned");
|
||||
|
||||
if !stores.contains_key(&path) {
|
||||
if collection.frozen {
|
||||
return Err(Error::NotFound(path));
|
||||
}
|
||||
let mut store = StoreBuilder::new(path.clone()).build();
|
||||
// ignore loading errors, just use the default
|
||||
if let Err(err) = store.load(app) {
|
||||
warn!(
|
||||
"Failed to load store {:?} from disk: {}. Falling back to default values.",
|
||||
path, err
|
||||
);
|
||||
}
|
||||
stores.insert(path.clone(), store);
|
||||
}
|
||||
|
||||
f(stores
|
||||
.get_mut(&path)
|
||||
.expect("failed to retrieve store. This is a bug!"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn set<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
window: Window<R>,
|
||||
stores: State<'_, StoreCollection>,
|
||||
path: PathBuf,
|
||||
key: String,
|
||||
value: JsonValue,
|
||||
) -> Result<(), Error> {
|
||||
with_store(&app, stores, path.clone(), |store| {
|
||||
store.cache.insert(key.clone(), value.clone());
|
||||
let _ = window.emit("store://change", ChangePayload { path, key, value });
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
stores: State<'_, StoreCollection>,
|
||||
path: PathBuf,
|
||||
key: String,
|
||||
) -> Result<Option<JsonValue>, Error> {
|
||||
with_store(&app, stores, path, |store| {
|
||||
Ok(store.cache.get(&key).cloned())
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn has<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
stores: State<'_, StoreCollection>,
|
||||
path: PathBuf,
|
||||
key: String,
|
||||
) -> Result<bool, Error> {
|
||||
with_store(&app, stores, path, |store| {
|
||||
Ok(store.cache.contains_key(&key))
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn delete<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
window: Window<R>,
|
||||
stores: State<'_, StoreCollection>,
|
||||
path: PathBuf,
|
||||
key: String,
|
||||
) -> Result<bool, Error> {
|
||||
with_store(&app, stores, path.clone(), |store| {
|
||||
let flag = store.cache.remove(&key).is_some();
|
||||
if flag {
|
||||
let _ = window.emit(
|
||||
"store://change",
|
||||
ChangePayload {
|
||||
path,
|
||||
key,
|
||||
value: JsonValue::Null,
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(flag)
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn clear<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
window: Window<R>,
|
||||
stores: State<'_, StoreCollection>,
|
||||
path: PathBuf,
|
||||
) -> Result<(), Error> {
|
||||
with_store(&app, stores, path.clone(), |store| {
|
||||
let keys = store.cache.keys().cloned().collect::<Vec<String>>();
|
||||
store.cache.clear();
|
||||
for key in keys {
|
||||
let _ = window.emit(
|
||||
"store://change",
|
||||
ChangePayload {
|
||||
path: path.clone(),
|
||||
key,
|
||||
value: JsonValue::Null,
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn reset<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
window: Window<R>,
|
||||
collection: State<'_, StoreCollection>,
|
||||
path: PathBuf,
|
||||
) -> Result<(), Error> {
|
||||
let has_defaults = collection
|
||||
.stores
|
||||
.lock()
|
||||
.expect("mutex poisoned")
|
||||
.get(&path)
|
||||
.map(|store| store.defaults.is_some());
|
||||
|
||||
if Some(true) == has_defaults {
|
||||
with_store(&app, collection, path.clone(), |store| {
|
||||
if let Some(defaults) = &store.defaults {
|
||||
for (key, value) in &store.cache {
|
||||
if defaults.get(key) != Some(value) {
|
||||
let _ = window.emit(
|
||||
"store://change",
|
||||
ChangePayload {
|
||||
path: path.clone(),
|
||||
key: key.clone(),
|
||||
value: defaults.get(key).cloned().unwrap_or(JsonValue::Null),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
store.cache = defaults.clone();
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
clear(app, window, collection, path).await
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn keys<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
stores: State<'_, StoreCollection>,
|
||||
path: PathBuf,
|
||||
) -> Result<Vec<String>, Error> {
|
||||
with_store(&app, stores, path, |store| {
|
||||
Ok(store.cache.keys().cloned().collect())
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn values<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
stores: State<'_, StoreCollection>,
|
||||
path: PathBuf,
|
||||
) -> Result<Vec<JsonValue>, Error> {
|
||||
with_store(&app, stores, path, |store| {
|
||||
Ok(store.cache.values().cloned().collect())
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn entries<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
stores: State<'_, StoreCollection>,
|
||||
path: PathBuf,
|
||||
) -> Result<Vec<(String, JsonValue)>, Error> {
|
||||
with_store(&app, stores, path, |store| {
|
||||
Ok(store.cache.clone().into_iter().collect())
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn length<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
stores: State<'_, StoreCollection>,
|
||||
path: PathBuf,
|
||||
) -> Result<usize, Error> {
|
||||
with_store(&app, stores, path, |store| Ok(store.cache.len()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn load<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
stores: State<'_, StoreCollection>,
|
||||
path: PathBuf,
|
||||
) -> Result<(), Error> {
|
||||
with_store(&app, stores, path, |store| store.load(&app))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn save<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
stores: State<'_, StoreCollection>,
|
||||
path: PathBuf,
|
||||
) -> Result<(), Error> {
|
||||
with_store(&app, stores, path, |store| store.save(&app))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PluginBuilder {
|
||||
stores: HashMap<PathBuf, Store>,
|
||||
frozen: bool,
|
||||
}
|
||||
|
||||
impl PluginBuilder {
|
||||
/// Registers a store with the plugin.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// use tauri_plugin_store::{StoreBuilder,PluginBuilder};
|
||||
///
|
||||
/// let store = StoreBuilder::new("store.bin".parse()?).build();
|
||||
///
|
||||
/// let builder = PluginBuilder::default().store(store);
|
||||
///
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn store(mut self, store: Store) -> Self {
|
||||
self.stores.insert(store.path.clone(), store);
|
||||
self
|
||||
}
|
||||
|
||||
/// Registers multiple stores with the plugin.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// use tauri_plugin_store::{StoreBuilder,PluginBuilder};
|
||||
///
|
||||
/// let store = StoreBuilder::new("store.bin".parse()?).build();
|
||||
///
|
||||
/// let builder = PluginBuilder::default().stores([store]);
|
||||
///
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn stores<T: IntoIterator<Item = Store>>(mut self, stores: T) -> Self {
|
||||
self.stores = stores
|
||||
.into_iter()
|
||||
.map(|store| (store.path.clone(), store))
|
||||
.collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Freezes the collection.
|
||||
///
|
||||
/// This causes requests for plugins that haven't been registered to fail
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// use tauri_plugin_store::{StoreBuilder,PluginBuilder};
|
||||
///
|
||||
/// let store = StoreBuilder::new("store.bin".parse()?).build();
|
||||
///
|
||||
/// let builder = PluginBuilder::default().freeze();
|
||||
///
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn freeze(mut self) -> Self {
|
||||
self.frozen = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the plugin.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// use tauri_plugin_store::{StoreBuilder,PluginBuilder};
|
||||
/// use tauri::Wry;
|
||||
///
|
||||
/// let store = StoreBuilder::new("store.bin".parse()?).build();
|
||||
///
|
||||
/// let plugin = PluginBuilder::default().build::<Wry>();
|
||||
///
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
|
||||
plugin::Builder::new("store")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
set, get, has, delete, clear, reset, keys, values, length, entries, load, save
|
||||
])
|
||||
.setup(move |app_handle| {
|
||||
for (path, store) in self.stores.iter_mut() {
|
||||
// ignore loading errors, just use the default
|
||||
if let Err(err) = store.load(app_handle) {
|
||||
warn!(
|
||||
"Failed to load store {:?} from disk: {}. Falling back to default values.",
|
||||
path, err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
app_handle.manage(StoreCollection {
|
||||
stores: Mutex::new(self.stores),
|
||||
frozen: self.frozen,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_event(|app_handle, event| {
|
||||
if let RunEvent::Exit = event {
|
||||
let collection = app_handle.state::<StoreCollection>();
|
||||
|
||||
for store in collection.stores.lock().expect("mutex poisoned").values() {
|
||||
if let Err(err) = store.save(app_handle) {
|
||||
eprintln!("failed to save store {:?} with error {:?}", store.path, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue