>백엔드 개발 >Golang >미니멀한 비밀번호 관리자 데스크탑 앱: Golang의 Wails 프레임워크 진출(1부)

미니멀한 비밀번호 관리자 데스크탑 앱: Golang의 Wails 프레임워크 진출(1부)

Mary-Kate Olsen
Mary-Kate Olsen원래의
2024-12-20 10:22:10473검색

I - 오늘날 데스크탑 애플리케이션을 개발하는 이유는 무엇입니까?

이것은 모든 개발자, 특히 webdev 세계에 종사하는 개발자라면 스스로에게 물어본 질문입니다. "브라우저에서 렌더링하고 내가 원하는 거의 모든 목적을 제공하는 거의 모든 것을 실행할 수 있다면, 누가 우리 애플리케이션을 다운로드하여 자신의 컴퓨터에서 실행해야 합니까?" 그러나 우리가 수행하는 작업의 명백한 요구 사항(예: 모든 OS 기능을 사용할 수 있는 것, 더 나은 성능, 오프라인 기능, 향상된 보안 및 통합 등)을 위해 우리 자신이나 회사를 위한 것 외에도 개발자로서 항상 우리를 풍요롭게 해줄 프로그래밍의 새로운 측면을 접하면서 얻는 경험입니다.

나처럼 Golang에 대한 열정이 있고 이 언어로 백엔드를 개발했지만 HTML, CSS 및 JavaScript(또는 일부 프레임워크)를 사용한 프런트엔드도 수행했다면 이 게시물이 적합합니다. 새로운 기술을 배우려면 데스크톱 애플리케이션을 만들 수 있는 능력이 더 필요합니다.

II - 정답은 Wails

이미 Electron이나 Tauri를 알고 계실 수도 있습니다. 둘 다 프런트엔드에 웹 기술을 사용합니다. 첫 번째는 백엔드에서 JavaScript(또는 NodeJs)를 사용하고 두 번째는 Rust를 사용합니다. 그러나 둘 다 다소 눈에 띄는 단점이 있습니다. Electron 앱은 매우 큰 바이너리를 가지고 있으며(전체 Chromium 브라우저를 패키지하기 때문에) 많은 메모리를 소비합니다. Tauri 앱은 이러한 측면을 개선하지만(무엇보다도 Chromium 대신 WebView2 [Windows]/WebKit [macOS 및 Linux]을 사용하기 때문에), 바이너리는 여전히 상대적으로 크고 컴파일 시간도… Rust의 시간인가요? (그들의 학습 곡선은 말할 것도 없고, 비록 제가 Rust를 좋아하지만 진심입니다 ?).

Wails를 사용하면 제가 방금 설명한 웹 기술을 통해 데스크톱 애플리케이션 개발의 모든 이점을 누릴 수 있을 뿐만 아니라 Go를 사용하여 얻을 수 있는 모든 이점도 얻을 수 있습니다.

  • 배우기 쉽고 표현력이 뛰어난 언어
  • 빠른 실행과 무엇보다도 빠른 컴파일
  • "즉시 사용 가능한" 크로스 편집,
  • 적당한 메모리 소비로 실행되는 소형 바이너리(예를 들어 여기서 개발할 애플리케이션은 Electron을 사용하면 ~100Mb, Fyne과 같은 다른 기본 GUI 프레임워크를 사용하면 ~20Mb, Wails를 사용하면 4Mb에 불과합니다. !!),
  • 사용자 경험을 향상시키는 "현대적인" UI를 쉽게 디자인할 수 있는 원하는 웹 프레임워크(Vanilla JS도 포함)를 사용할 수 있습니다.

예, Go를 사용하여 데스크톱 애플리케이션을 만들고 싶다면 다른 가능성이 있습니다(기본이든 아니든). Fynego-gtk를 언급하겠습니다. Fyne은 기본 앱을 쉽게 만들 수 있는 GUI 프레임워크입니다. 우아한 디자인을 갖고 있더라도 프레임워크의 기능이 다소 제한되거나 개발자가 동일한 기능을 달성하려면 많은 노력이 필요합니다. 다른 도구 및/또는 언어를 사용하면 쉽게 할 수 있습니다. GTK에 대한 Go 바인딩인 go-gtk에 대해서도 동일하게 말할 수 있습니다. 예, 자신의 능력에 한계가 있는 기본 애플리케이션을 얻게 될 것이라는 것은 사실입니다. 하지만 GTK 라이브러리에 들어가는 것은 정글을 탐험하는 것과 같습니다.…

III - Wails에 대한 접근 방식: Nu-i uita - 미니멀리스트 비밀번호 관리자

먼저 Nu-i uita가 무엇을 의미하는지 궁금하신 분들을 위해 루마니아어로 대략 "그들을 잊지 마세요"라는 뜻입니다. 원래 이름인줄 알았는데...

이 GitHub 저장소에서 애플리케이션의 전체 코드를 볼 수 있습니다. 지금 바로 사용해 보고 싶다면 여기(Windows 및 Linux용)에서 실행 파일을 다운로드할 수 있습니다.

A minimalist password manager desktop app: a foray into Golang

애플리케이션 작동 방식을 간략하게 설명하겠습니다. 사용자가 처음으로 로그인하면 로그인 창에서 마스터 비밀번호를 입력하라는 메시지를 표시합니다. 이는 비밀번호 자체를 암호화 키로 사용하여 암호화되어 저장됩니다. 이 로그인 창은 사용자가 해당 웹사이트에 대해 저장된 비밀번호와 사용된 사용자 이름을 나열할 수 있는 또 다른 인터페이스로 연결됩니다(이 목록에서 사용자 이름이나 웹사이트로 검색할 수도 있습니다). 목록의 각 항목을 클릭하여 세부정보를 확인하고, 클립보드에 복사하고, 편집하거나 삭제할 수 있습니다. 또한 새 항목을 추가할 때 마스터 비밀번호를 키로 사용하여 비밀번호를 암호화합니다. 구성 창에서 원하는 언어(현재는 영어와 스페인어만 가능)를 선택하고, 저장된 모든 데이터를 삭제하고, 내보내거나 백업 파일에서 가져올 수 있습니다. 데이터를 가져올 때 사용자에게 내보내기를 수행할 때 사용된 마스터 비밀번호를 묻는 메시지가 표시되며, 이제 가져온 데이터는 현재 마스터 비밀번호로 저장되고 암호화됩니다. 이후 사용자가 애플리케이션에 다시 로그인할 때마다 현재 마스터 비밀번호를 입력하라는 메시지가 표시됩니다.

Wails를 사용하는 데 필요한 요구 사항은 훌륭한 문서에 잘 설명되어 있으므로 자세히 설명하지 않겠습니다. 어떤 경우든 강력한 CLI를 설치하는 것이 중요합니다(github.com/wailsapp/wails/v2/cmd/wails@latest 설치). 이를 통해 애플리케이션용 스캐폴딩을 생성하고 코드 편집 시 핫 리로드할 수 있습니다. , 실행 파일을 빌드합니다(크로스 컴파일 포함).

Wails CLI를 사용하면 다양한 프런트엔드 프레임워크로 프로젝트를 생성할 수 있지만, 어떤 이유로 Wails 제작자는 Svelte를 선호하는 것 같습니다... 왜냐하면 그것이 그들이 언급하는 첫 번째 옵션이기 때문입니다. wails init -n myproject -t svelte-ts 명령을 사용하면 Svelte3TypeScript를 사용하여 프로젝트를 생성합니다.

어떤 이유로 Svelte5의 새로운 룬 시스템 기능을 사용하고 싶다면 다음을 사용하여 프로젝트 생성을 자동화하는 bash 스크립트를 만들었습니다. 날씬한5. 이 경우 Wails CLI도 설치해야 합니다.

위에 언급한 애플리케이션의 기능은 모든 todoapp(프로그래밍에서 새로운 것을 배울 수 있는 좋은 방법임)의 요구 사항을 구성하지만 여기에 plus를 추가합니다. 기능을 좀 더 향상시키는 기능(예: 백엔드에서는 대칭 암호화 사용, 프론트엔드에서는 국제화 사용) 단순한 할일앱보다 유용하고 유익합니다.

자, 소개는 이쯤하고 본론으로 들어가볼까요?.

IV - Wails 프로젝트 구조: 이 프레임워크의 작동 방식에 대한 개요

Wails init -n myproject -t svelte-ts 명령을 실행하여 CLI에서 Svelte Typescript를 사용하여 Wails 프로젝트를 생성하기로 선택한 경우(또는 이전에 이미 말씀드린 bash 스크립트를 사용하여 생성) Svelte5를 사용한 Wails 프로젝트는 다음과 매우 유사한 디렉토리 구조를 갖게 됩니다.

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

방금 보신 것은 완성된 애플리케이션 구조입니다. Wails CLI에서 생성된 것과 유일한 차이점은 Svelte3 TypeScript 프런트엔드와 내 스크립트를 사용하여 Svelte5 외에도 Tailwindcss Daisyui를 사용하여 Wails 애플리케이션의 스캐폴딩을 얻을 수 있다는 점입니다. 통합되었습니다.

Wails 애플리케이션이 일반적으로 어떻게 작동하는지 살펴보는 동시에 우리 사례에 대한 설명을 구체적으로 살펴보겠습니다.

A minimalist password manager desktop app: a foray into Golang

Wails 문서에 따르면: "Wails 애플리케이션은 웹킷 프런트엔드를 갖춘 표준 Go 애플리케이션입니다. 애플리케이션의 Go 부분은 애플리케이션 코드와 런타임 라이브러리로 구성됩니다. 이는 애플리케이션 창 제어와 같은 여러 가지 유용한 작업을 제공합니다. 프런트엔드는 프런트엔드 자산을 표시하는 웹킷 창입니다. 간단히 말해서, 웹 기술을 사용하여 데스크톱 애플리케이션을 만들었다면 이미 알고 있듯이 매우 간략하게 설명하면 애플리케이션은 백엔드(우리의 경우 Go로 작성됨)와 그 자산이 Webkit 창(에서)으로 관리되는 프런트엔드로 구성됩니다. Windows OS인 Webview2)의 경우 프런트엔드 자산을 제공/렌더링하는 웹 서버/브라우저의 본질과 같습니다.

Windows와 Linux 모두에서 애플리케이션을 실행할 수 있기를 원하는 특정 사례의 기본 애플리케이션은 다음 코드로 구성됩니다.

/* main.go */

package main

import (
    "embed"

    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"
    "github.com/wailsapp/wails/v2/pkg/options/linux"
)

//go:embed all:frontend/dist
var assets embed.FS

//go:embed build/appicon.png
var icon []byte

func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        // Title:         "Nu-i uita • minimalist password manager",
        Width:         450,
        Height:        300,
        DisableResize: true,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
        OnStartup:        app.startup,
        OnBeforeClose:    app.beforeClose,
        Bind: []interface{}{
            app,
        },
        // Linux platform specific options
        Linux: &linux.Options{
            Icon: icon,
            // WindowIsTranslucent: true,
            WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
            // ProgramName:         "wails",
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

가장 먼저 해야 할 일은 Go Context가 있는 필드가 있어야 하는 App을 호출하기로 동의한 구조체를 인스턴스화하는 것입니다(NewApp 함수 사용). 그러면 wails의 Run 메소드가 애플리케이션을 시작하는 메소드입니다. 일련의 옵션을 전달해야 합니다. 이러한 필수 옵션 중 하나는 자산입니다. Wails는 프런트엔드를 컴파일하고 나면 이를 "frontend/dist" 폴더에 생성합니다. //go:embed all:frontend/dist 지시문(Go의 마법 같은 기능)을 사용하여 전체 프런트엔드를 최종 실행 파일에 포함할 수 있습니다. Linux의 경우 애플리케이션 아이콘을 삽입하려면 //go:embed 지시문도 사용해야 합니다.

문서에서 확인할 수 있는 나머지 옵션에 대해서는 다루지 않겠습니다. 옵션과 관련해서 두 가지만 말씀드리겠습니다. 첫 번째는 애플리케이션의 제목 표시줄에 나타나는 제목을 여기에서 옵션으로 설정할 수 있다는 것입니다. 하지만 사용자가 원하는 언어를 선택할 수 있는 애플리케이션에서는 메시지를 받을 때 이를 설정합니다(Wails 런타임을 사용하여). 사용자가 할 수 있는 언어 변경 이벤트입니다. 나중에 살펴보겠습니다.

두 번째로 중요한 옵션 관련 문제는 바인딩 옵션입니다. 문서에는 그 의미가 매우 잘 설명되어 있습니다. "바인드 옵션은 Wails 애플리케이션에서 가장 중요한 옵션 중 하나입니다. 프런트엔드에 노출할 구조체 메서드를 지정합니다. 기존 웹의 컨트롤러와 같은 구조체를 생각해 보세요. 애플리케이션." 실제로 백엔드를 프런트엔드에 노출시키는 App 구조의 공개 메소드는 Go를 JavaScript와 "연결"하는 마법을 수행합니다. 해당 구조체의 공개 메소드는 Wails가 수행하는 컴파일을 통해 Promise를 반환하는 JavaScript 함수로 변환됩니다.

백엔드와 프런트엔드 간의 또 다른 중요한 통신 형태(이 애플리케이션에서 효과적으로 사용함)는 이벤트입니다. Wails는 Go 또는 JavaScript로 이벤트를 내보내거나 받을 수 있는 이벤트 시스템을 제공합니다. 선택적으로 데이터를 이벤트와 함께 전달할 수 있습니다. 애플리케이션에서 이벤트를 사용하는 방식을 연구하면 앱 구조를 분석할 수 있습니다.

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

우리가 가장 먼저 보는 것은 Wails에 필요한 Go Context를 저장하는 필드와 구조체 Db에 대한 포인터(우리가 볼 수 있듯이 데이터베이스와 관련됨)가 있는 구조체 앱입니다. . 다른 2개의 속성은 사용자가 선택한 언어에 따라 기본 대화 상자(백엔드에서 관리됨)가 제목을 표시하도록 구성하는 문자열입니다. App의 생성자(NewApp) 역할을 하는 함수는 단순히 데이터베이스 구조에 대한 포인터를 생성합니다.

다음으로 Wails에 필요한 옵션에 필요한 두 가지 메서드인 startupbeforeClose를 확인합니다. 이를 각각 OnStartup에 전달합니다. OnBeforeClose 옵션. 그렇게 하면 자동으로 Go Context를 받게 됩니다. beforeClose는 애플리케이션을 닫을 때 단순히 데이터베이스에 대한 연결을 닫습니다. 하지만 스타트업은 그 이상입니다. 먼저 해당 필드에서 수신하는 컨텍스트를 설정합니다. 둘째, 일련의 작업을 실행하는 데 필요한 일련의 이벤트 리스너를 백엔드에 등록합니다.

In Wails의 모든 이벤트 청취자는 다음 서명을 갖습니다.

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

즉, 컨텍스트(App의 ctx 필드에 저장한 것), 이벤트 이름(프론트엔드에서 설정할 것) 및 실행할 콜백을 수신합니다. 우리가 필요로 하는 작업과 any 유형 또는 빈 인터페이스(인터페이스{})의 선택적 매개변수를 받을 수 있는 작업은 동일하므로 유형 어설션을 만들어야 합니다.

우리가 선언한 일부 리스너에는 프런트엔드에서 수신되고 그곳에서 특정 작업을 트리거할 중첩된 이벤트 이미터가 선언되어 있습니다. 그의 서명은 다음과 같습니다.

/* main.go */

package main

import (
    "embed"

    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"
    "github.com/wailsapp/wails/v2/pkg/options/linux"
)

//go:embed all:frontend/dist
var assets embed.FS

//go:embed build/appicon.png
var icon []byte

func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        // Title:         "Nu-i uita • minimalist password manager",
        Width:         450,
        Height:        300,
        DisableResize: true,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
        OnStartup:        app.startup,
        OnBeforeClose:    app.beforeClose,
        Bind: []interface{}{
            app,
        },
        // Linux platform specific options
        Linux: &linux.Options{
            Icon: icon,
            // WindowIsTranslucent: true,
            WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
            // ProgramName:         "wails",
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

간결함을 위해서뿐만 아니라 코드만 읽어도 그들이 무엇을 하는지 알 수 있을 만큼 Go의 표현력이 풍부하기 때문에 이 청취자들이 무엇을 하는지 자세히 설명하지 않겠습니다. 그 중 몇 가지만 설명하겠습니다. "change_titles" 리스너는 해당 이름의 이벤트를 수신할 것으로 예상합니다. 이 이벤트는 사용자가 인터페이스 언어를 변경하여 응용 프로그램 창 제목 표시줄의 제목을 리스너 자체에서 받은 값으로 변경할 때 트리거됩니다. 이를 달성하기 위해 Wails 런타임 패키지를 사용합니다. 이벤트는 필요할 때 사용할 앱 구조의 별도 속성에 저장되는 "디렉터리 선택" 및 "파일 선택" 대화 상자의 제목도 수신합니다. 보시다시피 이러한 "네이티브" 작업은 백엔드에서 수행되어야 하기 때문에 이 이벤트가 필요합니다.

말하자면 연결된 청취자 "import_data" 및 "password"에 대한 특별 언급입니다. 첫 번째 항목("import_data")은 수신되면 런타임.OpenFileDialog 메서드를 사용하여 대화 상자 열기를 트리거합니다. 보시다시피, 이 메서드는 옵션 중에서 표시할 제목을 수신하며, 이는 이미 설명한 대로 App 구조체의 selectedFile 필드에 저장됩니다. 사용자가 파일을 선택하여 fileLocation 변수가 비어 있지 않으면 프론트엔드에서 수신되는 이벤트("enter_password"라고 함)가 발생하여 사용자에게 마스터 비밀번호를 입력하라는 팝업을 표시합니다. 수출할 때 사용했습니다. 사용자가 그렇게 하면 프런트엔드는 백엔드 리스너에서 수신하는 이벤트("비밀번호")를 내보냅니다. 수신된 데이터(마스터 비밀번호)와 백업 파일 경로는 데이터베이스를 대표하는 Db 구조체의 메소드(ImportDump)로 사용된다. 해당 메서드의 실행 결과에 따라 가져오기 성공 또는 실패 결과와 함께 프런트엔드에 팝업 창을 트리거하는 새 이벤트("imported_data")가 발생합니다.

보시다시피 Wails 이벤트는 백엔드와 프론트엔드 간의 강력하고 효과적인 커뮤니케이션 방법입니다.

App 구조체의 나머지 메서드는 이미 설명했듯이 백엔드가 프런트엔드에 노출하는 메서드에 지나지 않으며 기본적으로 데이터베이스를 사용한 CRUD 작업입니다. 그래서 아래에서 설명드리겠습니다.

V - 백엔드: 앱 배관

이 백엔드 부분에서는 vikkio88의 이 게시물(여기 DEV.to)과 그의 비밀번호 관리자 저장소에서 영감을 얻었습니다(일부 수정). 이 게시물은 C#/Avalonia로 처음 만든 후 Go/를 사용하도록 조정했습니다. Fyne (Muscurd-ig).

백엔드의 '최하위 수준' 부분은 비밀번호 암호화와 관련된 부분입니다. 가장 필수적인 것은 다음 3가지 기능입니다:

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

Go에서 AES를 사용한 대칭 암호화에 대해 자세히 설명하지 않겠습니다. 당신이 알아야 할 모든 것은 DEV.to의 이 게시물에 잘 설명되어 있습니다.

AES는 고정 크기 키와 고정 크기 일반 텍스트를 가져와 고정 크기 암호 텍스트를 반환하는 블록 암호화 알고리즘입니다. AES의 블록 크기는 16바이트로 설정되어 있으므로 일반 텍스트의 길이는 16바이트 이상이어야 합니다. 우리는 임의의 크기의 데이터를 암호화/해독할 수 있기를 원하기 때문에 문제가 발생합니다. 최소 일반 텍스트 블록 크기 문제를 해결하기 위해 블록 암호화 모드가 존재합니다. 여기서는 가장 널리 채택되는 대칭 블록 암호화 모드 중 하나인 GCM 모드를 사용합니다. GCM에는 항상 무작위로 생성되어야 하는 IV(초기화 벡터[배열])가 필요합니다(이러한 배열에 사용되는 용어는 nonce입니다).

기본적으로 암호화 함수는 암호화할 일반 텍스트와 항상 32바이트 길이의 비밀 키를 사용하고 해당 키를 사용하여 AES 암호화기를 생성합니다. 해당 암호화기를 사용하여 12바이트 초기화 벡터(nonce)를 생성하는 데 사용하는 gcm 개체를 생성합니다. gcm 개체의 Seal 메서드를 사용하면 nonce 벡터를 사용하여 일반 텍스트(바이트 조각)를 "결합"하고 암호화한 다음 마지막으로 결과를 다시 문자열로 변환할 수 있습니다.

decrypt 함수는 그 반대입니다. 함수의 첫 번째 부분은 encrypt와 같습니다. 그러면 암호문이 실제로 nonce 암호문임을 알기 때문에 암호문을 다음과 같이 분할할 수 있습니다. 2개의 구성 요소입니다. gcm 개체의 NonceSize 메서드는 항상 "12"(nonce의 길이)를 반환하므로 이를 해독하는 동시에 바이트 슬라이스를 분할합니다. gcm 객체의 Open 메소드를 사용합니다. 마지막으로 결과를 문자열로 변환합니다.

keyfy 함수는 32바이트 비밀 키를 보장합니다(해당 길이에 도달하려면 "0"을 채워야 함). 프런트엔드에서는 사용자가 1바이트 이상의 문자(ASCII가 아닌 문자)를 입력하지 않도록 하여 이 함수의 결과가 항상 32바이트가 되도록 하는 것을 확인합니다.

이 파일의 나머지 코드는 위에서 설명한 함수의 입력/출력을 base64로 인코딩/디코딩하는 역할을 본질적으로 담당합니다.

모든 애플리케이션 데이터를 저장하기 위해 cloverDB를 사용합니다. MongoDB와 유사한 경량의 임베디드 문서 중심의 NoSQL 데이터베이스입니다. 이 데이터베이스의 기능 중 하나는 레코드가 저장될 때 uuid 문자열인 ID(기본적으로 필드는 MongoDB에서 발생하는 것과 약간 유사한 _id로 지정됨)가 할당된다는 것입니다( v4). 따라서 항목 순서에 따라 레코드를 정렬하려면 저장 시 타임스탬프를 할당해야 합니다.

이러한 사실을 바탕으로 모델과 관련 메서드(master_password.go &password_entry.go)를 만듭니다.

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

MasterPassword에는 데이터베이스에 저장/검색되지 않는(따라서 클로버 태그가 없는) 개인 필드(클리어)가 있습니다. 즉, 메모리에만 상주하고 디스크에 저장되지 않습니다. 이 속성은 암호화되지 않은 마스터 비밀번호 자체이며 비밀번호 항목의 암호화 키로 사용됩니다. 이 값은 MasterPassword 개체의 setter에 의해 저장되거나 동일한 이름의 패키지 구조체 Db에 있는 내보내지 않은(개인) 필드로 (콜백에 의해) 설정됩니다. (db.go). 비밀번호 입력을 위해 우리는 2개의 구조체를 사용합니다. 하나는 암호화된 비밀번호가 없고 다른 하나는 비밀번호가 이미 암호화되어 있으며, 이는 실제로 데이터베이스에 저장될 객체입니다(DTO와 유사). , 데이터 전송 개체). 두 구조체의 암호화/복호화 방법은 내부적으로 암호화 키(32바이트 긴 슬라이스로 변환된 마스터 비밀번호)가 있는 속성을 갖는 Crypto 개체를 사용합니다.

/* main.go */

package main

import (
    "embed"

    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"
    "github.com/wailsapp/wails/v2/pkg/options/linux"
)

//go:embed all:frontend/dist
var assets embed.FS

//go:embed build/appicon.png
var icon []byte

func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        // Title:         "Nu-i uita • minimalist password manager",
        Width:         450,
        Height:        300,
        DisableResize: true,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
        OnStartup:        app.startup,
        OnBeforeClose:    app.beforeClose,
        Bind: []interface{}{
            app,
        },
        // Linux platform specific options
        Linux: &linux.Options{
            Icon: icon,
            // WindowIsTranslucent: true,
            WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
            // ProgramName:         "wails",
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

마스터 비밀번호에는 데이터 저장/복구에 중요한 역할을 하는 3가지 방법이 있습니다.

/* app.go */

package main

import (
    "context"

    "github.com/emarifer/Nu-i-uita/internal/db"
    "github.com/emarifer/Nu-i-uita/internal/models"
    "github.com/wailsapp/wails/v2/pkg/runtime"
)

// App struct
type App struct {
    ctx               context.Context
    db                *db.Db
    selectedDirectory string
    selectedFile      string
}

// NewApp creates a new App application struct
func NewApp() *App {
    db := db.NewDb()

    return &App{db: db}
}

// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
    var fileLocation string
    a.ctx = ctx

    runtime.EventsOn(a.ctx, "change_titles", func(optionalData ...interface{}) {
        if appTitle, ok := optionalData[0].(string); ok {
            runtime.WindowSetTitle(a.ctx, appTitle)
        }
        if selectedDirectory, ok := optionalData[1].(string); ok {
            a.selectedDirectory = selectedDirectory
        }
        if selectedFile, ok := optionalData[2].(string); ok {
            a.selectedFile = selectedFile
        }
    })

    runtime.EventsOn(a.ctx, "quit", func(optionalData ...interface{}) {
        runtime.Quit(a.ctx)
    })

    runtime.EventsOn(a.ctx, "export_data", func(optionalData ...interface{}) {
        d, _ := runtime.OpenDirectoryDialog(a.ctx, runtime.
            OpenDialogOptions{
            Title: a.selectedDirectory,
        })

        if d != "" {
            f, err := a.db.GenerateDump(d)
            if err != nil {
                runtime.EventsEmit(a.ctx, "saved_as", err.Error())
                return
            }
            runtime.EventsEmit(a.ctx, "saved_as", f)
        }
    })

    runtime.EventsOn(a.ctx, "import_data", func(optionalData ...interface{}) {
        fileLocation, _ = runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
            Title: a.selectedFile,
        })

        // fmt.Println("SELECTED FILE:", fileLocation)
        if fileLocation != "" {
            runtime.EventsEmit(a.ctx, "enter_password")
        }
    })

    runtime.EventsOn(a.ctx, "password", func(optionalData ...interface{}) {
        // fmt.Printf("MY PASS: %v", optionalData...)
        if pass, ok := optionalData[0].(string); ok {
            if len(fileLocation) != 0 {
                err := a.db.ImportDump(pass, fileLocation)
                if err != nil {
                    runtime.EventsEmit(a.ctx, "imported_data", err.Error())
                    return
                }
                runtime.EventsEmit(a.ctx, "imported_data", "success")
            }
        }
    })
}

// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
    defer a.db.Close()

    return false
}

...

GetCrypto를 사용하면 db.go 패키지가 비밀번호 항목을 암호화/해독할 수 있도록 Crypto 개체의 현재 인스턴스를 가져올 수 있습니다. SetClear는 앞서 말씀드린 setter이고, Check는 사용자가 입력한 마스터 비밀번호가 맞는지 확인하는 기능입니다. 보시다시피 비밀번호 외에도 콜백을 인수로 사용하며, 경우에 따라 앞서 언급한 setter가 됩니다(백업 파일에서 데이터를 가져올 때). ) 또는 사용자가 로그인할 때 Db 구조체의 private 필드에 값을 설정하는 db.go 패키지의 SetMasterPassword 메서드.

db.go 패키지의 모든 메소드를 자세히 설명하지는 않겠습니다. 대부분의 코드가 cloverDB의 작업 방식과 관련되어 있기 때문입니다. 해당 패키지의 문서에서 확인할 수 있습니다. 여기서 사용될 몇 가지 중요한 사항을 이미 언급했지만.

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

먼저 cloverDB 인스턴스에 대한 포인터를 저장할 구조체가 있습니다. 또한 MasterPassword 구조체의 "전체" 인스턴스에 대한 포인터를 저장합니다. 여기서 "전체"는 암호화된 마스터 비밀번호(데이터베이스에 존재하므로 현재 마스터 비밀번호임)와 비밀번호 항목 암호화에 사용되는 암호화되지 않은 마스터 비밀번호를 모두 저장한다는 의미입니다. 다음으로 setupCollections, NewDb, Close가 있는데, 이는 애플리케이션이 시작되고 닫힐 때 데이터베이스를 설정하는 함수 및 메서드입니다. cloverDB는 Open 메소드로 인스턴스화할 때 스토리지 파일/디렉토리를 자동으로 생성하지 않으며 대신 수동으로 생성해야 합니다. 마지막으로 GetLanguageCodeSaveLanguageCode는 사용자가 선택한 응용 프로그램 언어를 검색/저장하는 메소드입니다. 선택한 언어 코드는 작은 문자열("en" 또는 "es")이므로 단순화를 위해 구조체를 사용하여 이를 저장하지 않습니다. 예를 들어 컬렉션에서 언어 코드를 검색합니다(cloverDB는 "documents"와 함께 작동함). MongoDB와 유사하게 "컬렉션"), 저장된 키("코드")를 전달하고 유형을 만듭니다. 주장.

사용자가 처음으로 로그인하면 마스터 비밀번호가 데이터베이스에 저장됩니다. 이 값(암호화되지 않음)은 이미 암호화된 비밀번호도 저장하는 MasterPassword 개체의 일반 필드에 설정됩니다. 비밀번호는 Db 구조체에 저장됩니다:

/* main.go */

package main

import (
    "embed"

    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"
    "github.com/wailsapp/wails/v2/pkg/options/linux"
)

//go:embed all:frontend/dist
var assets embed.FS

//go:embed build/appicon.png
var icon []byte

func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        // Title:         "Nu-i uita • minimalist password manager",
        Width:         450,
        Height:        300,
        DisableResize: true,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
        OnStartup:        app.startup,
        OnBeforeClose:    app.beforeClose,
        Bind: []interface{}{
            app,
        },
        // Linux platform specific options
        Linux: &linux.Options{
            Icon: icon,
            // WindowIsTranslucent: true,
            WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
            // ProgramName:         "wails",
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

애플리케이션을 시작할 때 마스터 비밀번호 복구가 두 번 수행됩니다.

  1. 데이터베이스에 저장된 마스터 비밀번호가 있는지 확인하려면
  2. MasterPassword 인스턴스를 얻고 해당 Check 방법을 사용하여 사용자가 제공한 비밀번호가 올바른지 확인합니다.

두 경우 모두 RecoverMasterPassword 메서드가 호출됩니다. 이 메서드는 저장된 마스터 비밀번호가 있는 경우에만 Db 구조체의 캐시된Mp 필드에 인스턴스를 설정합니다.

/* app.go */

package main

import (
    "context"

    "github.com/emarifer/Nu-i-uita/internal/db"
    "github.com/emarifer/Nu-i-uita/internal/models"
    "github.com/wailsapp/wails/v2/pkg/runtime"
)

// App struct
type App struct {
    ctx               context.Context
    db                *db.Db
    selectedDirectory string
    selectedFile      string
}

// NewApp creates a new App application struct
func NewApp() *App {
    db := db.NewDb()

    return &App{db: db}
}

// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
    var fileLocation string
    a.ctx = ctx

    runtime.EventsOn(a.ctx, "change_titles", func(optionalData ...interface{}) {
        if appTitle, ok := optionalData[0].(string); ok {
            runtime.WindowSetTitle(a.ctx, appTitle)
        }
        if selectedDirectory, ok := optionalData[1].(string); ok {
            a.selectedDirectory = selectedDirectory
        }
        if selectedFile, ok := optionalData[2].(string); ok {
            a.selectedFile = selectedFile
        }
    })

    runtime.EventsOn(a.ctx, "quit", func(optionalData ...interface{}) {
        runtime.Quit(a.ctx)
    })

    runtime.EventsOn(a.ctx, "export_data", func(optionalData ...interface{}) {
        d, _ := runtime.OpenDirectoryDialog(a.ctx, runtime.
            OpenDialogOptions{
            Title: a.selectedDirectory,
        })

        if d != "" {
            f, err := a.db.GenerateDump(d)
            if err != nil {
                runtime.EventsEmit(a.ctx, "saved_as", err.Error())
                return
            }
            runtime.EventsEmit(a.ctx, "saved_as", f)
        }
    })

    runtime.EventsOn(a.ctx, "import_data", func(optionalData ...interface{}) {
        fileLocation, _ = runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
            Title: a.selectedFile,
        })

        // fmt.Println("SELECTED FILE:", fileLocation)
        if fileLocation != "" {
            runtime.EventsEmit(a.ctx, "enter_password")
        }
    })

    runtime.EventsOn(a.ctx, "password", func(optionalData ...interface{}) {
        // fmt.Printf("MY PASS: %v", optionalData...)
        if pass, ok := optionalData[0].(string); ok {
            if len(fileLocation) != 0 {
                err := a.db.ImportDump(pass, fileLocation)
                if err != nil {
                    runtime.EventsEmit(a.ctx, "imported_data", err.Error())
                    return
                }
                runtime.EventsEmit(a.ctx, "imported_data", "success")
            }
        }
    })
}

// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
    defer a.db.Close()

    return false
}

...

다음으로 작지만 중요한 두 가지 코드가 있습니다.

  1. SetMasterPassword는 앞서 언급했듯이 MasterPassword 개체의 Check 메서드에 대한 콜백으로 사용되며 암호화되지 않은 암호를 설정합니다. 해당 필드가 nil이 아닌 경우에만 Db 구조체의 cashedMp 필드에 마스터 비밀번호를 입력하세요.
  2. getCryptoInstance, 이는 캐시된Mp가 nil이 아닌 경우에만 Crypto 객체의 인스턴스를 반환합니다. 그렇지 않으면 애플리케이션에 패닉이 발생합니다. 이론상으로는 사용자가 애플리케이션에서 인증된 경우 이러한 상황이 발생할 수 없지만 보안상의 이유로 이러한 상황이 발생하면 애플리케이션을 종료합니다.
EventsOn(
    ctx context.Context,
    eventName string,
    callback func(optionalData ...interface{}),
) func()

모든 todoapp에서 일반적으로 사용되는 일반적인 CRUD 작업 외에도 설명할 수 있는 다른 기능이나 방법이 있습니다.

.
├── app.go
├── build
│   ├── appicon.png
│   ├── darwin
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── README.md
│   └── windows
│       ├── icon.ico
│       ├── info.json
│       ├── installer
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── frontend
│   ├── index.html
│   ├── package.json
│   ├── package.json.md5
│   ├── package-lock.json
│   ├── postcss.config.js
│   ├── README.md
│   ├── src
│   │   ├── App.svelte
│   │   ├── assets
│   │   │   ├── fonts
│   │   │   │   ├── nunito-v16-latin-regular.woff2
│   │   │   │   └── OFL.txt
│   │   │   └── images
│   │   │       └── logo-universal.png
│   │   ├── lib
│   │   │   ├── BackBtn.svelte
│   │   │   ├── BottomActions.svelte
│   │   │   ├── EditActions.svelte
│   │   │   ├── EntriesList.svelte
│   │   │   ├── Language.svelte
│   │   │   ├── popups
│   │   │   │   ├── alert-icons.ts
│   │   │   │   └── popups.ts
│   │   │   ├── ShowPasswordBtn.svelte
│   │   │   └── TopActions.svelte
│   │   ├── locales
│   │   │   ├── en.json
│   │   │   └── es.json
│   │   ├── main.ts
│   │   ├── pages
│   │   │   ├── About.svelte
│   │   │   ├── AddPassword.svelte
│   │   │   ├── Details.svelte
│   │   │   ├── EditPassword.svelte
│   │   │   ├── Home.svelte
│   │   │   ├── Login.svelte
│   │   │   └── Settings.svelte
│   │   ├── style.css
│   │   └── vite-env.d.ts
│   ├── svelte.config.js
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   ├── vite.config.ts
│   └── wailsjs
│       ├── go
│       │   ├── main
│       │   │   ├── App.d.ts
│       │   │   └── App.js
│       │   └── models.ts
│       └── runtime
│           ├── package.json
│           ├── runtime.d.ts
│           └── runtime.js
├── go.mod
├── go.sum
├── internal
│   ├── db
│   │   └── db.go
│   └── models
│       ├── crypto.go
│       ├── master_password.go
│       └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json

loadPasswordEntryDTO는 cloverDB에서 얻은 단일 문서에서 PasswordEntryDTO 객체를 생성하는 도우미 함수입니다. loadManyPasswordEntryDTO는 동일한 작업을 수행하지만 cloverDB 문서 조각에서 PasswordEntryDTO 조각을 생성합니다. 마지막으로 loadManyPasswordEntryloadManyPasswordEntryDTO와 동일하지만 getCryptoInstance에 의해 생성된 Crypto 객체의 인스턴스에서 cloverDB에서 얻은 문서를 해독합니다. 방법입니다.

마지막으로 CRUD와 관련되지 않은 방법 중 데이터 내보내기/가져오기에 사용되는 방법이 있습니다.

/* main.go */

package main

import (
    "embed"

    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"
    "github.com/wailsapp/wails/v2/pkg/options/linux"
)

//go:embed all:frontend/dist
var assets embed.FS

//go:embed build/appicon.png
var icon []byte

func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        // Title:         "Nu-i uita • minimalist password manager",
        Width:         450,
        Height:        300,
        DisableResize: true,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
        OnStartup:        app.startup,
        OnBeforeClose:    app.beforeClose,
        Bind: []interface{}{
            app,
        },
        // Linux platform specific options
        Linux: &linux.Options{
            Icon: icon,
            // WindowIsTranslucent: true,
            WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
            // ProgramName:         "wails",
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

GenerateDump는 백업 파일에 저장되는 개체가 될 DbDump 구조체를 사용합니다. 사용자가 선택한 디렉토리 경로, 날짜 형식 및 ad hoc 확장자를 이름으로 사용합니다. 그런 다음 암호화된 마스터 비밀번호, DTO 슬라이스(해당 비밀번호도 암호화됨) 및 사용자가 데이터베이스에 저장한 언어 코드를 사용하여 DbDump 인스턴스를 생성합니다. 이 객체는 최종적으로 우리가 생성한 파일의 Golang gob 패키지에 의해 바이너리로 인코딩되어 파일 이름을 UI에 반환하여 사용자에게 성공적인 생성을 알립니다.

반면 ImportDump는 UI가 사용자에게 묻는 마스터 비밀번호(내보내기를 수행할 때 적용되는 비밀번호)와 백업 파일의 경로를 인수로 사용합니다. 이제 DbDump 구조를 사용하여 선택한 파일의 암호를 해독한 다음 DbDump에 저장된 암호화된 마스터 비밀번호에서 MasterPassword 인스턴스를 얻습니다. 다음 단계에서는 MasterPassword 인스턴스에 지우기 필드를 설정하면서 사용자가 제공한 비밀번호가 올바른지 확인합니다.

/* app.go */

package main

import (
    "context"

    "github.com/emarifer/Nu-i-uita/internal/db"
    "github.com/emarifer/Nu-i-uita/internal/models"
    "github.com/wailsapp/wails/v2/pkg/runtime"
)

// App struct
type App struct {
    ctx               context.Context
    db                *db.Db
    selectedDirectory string
    selectedFile      string
}

// NewApp creates a new App application struct
func NewApp() *App {
    db := db.NewDb()

    return &App{db: db}
}

// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
    var fileLocation string
    a.ctx = ctx

    runtime.EventsOn(a.ctx, "change_titles", func(optionalData ...interface{}) {
        if appTitle, ok := optionalData[0].(string); ok {
            runtime.WindowSetTitle(a.ctx, appTitle)
        }
        if selectedDirectory, ok := optionalData[1].(string); ok {
            a.selectedDirectory = selectedDirectory
        }
        if selectedFile, ok := optionalData[2].(string); ok {
            a.selectedFile = selectedFile
        }
    })

    runtime.EventsOn(a.ctx, "quit", func(optionalData ...interface{}) {
        runtime.Quit(a.ctx)
    })

    runtime.EventsOn(a.ctx, "export_data", func(optionalData ...interface{}) {
        d, _ := runtime.OpenDirectoryDialog(a.ctx, runtime.
            OpenDialogOptions{
            Title: a.selectedDirectory,
        })

        if d != "" {
            f, err := a.db.GenerateDump(d)
            if err != nil {
                runtime.EventsEmit(a.ctx, "saved_as", err.Error())
                return
            }
            runtime.EventsEmit(a.ctx, "saved_as", f)
        }
    })

    runtime.EventsOn(a.ctx, "import_data", func(optionalData ...interface{}) {
        fileLocation, _ = runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
            Title: a.selectedFile,
        })

        // fmt.Println("SELECTED FILE:", fileLocation)
        if fileLocation != "" {
            runtime.EventsEmit(a.ctx, "enter_password")
        }
    })

    runtime.EventsOn(a.ctx, "password", func(optionalData ...interface{}) {
        // fmt.Printf("MY PASS: %v", optionalData...)
        if pass, ok := optionalData[0].(string); ok {
            if len(fileLocation) != 0 {
                err := a.db.ImportDump(pass, fileLocation)
                if err != nil {
                    runtime.EventsEmit(a.ctx, "imported_data", err.Error())
                    return
                }
                runtime.EventsEmit(a.ctx, "imported_data", "success")
            }
        }
    })
}

// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
    defer a.db.Close()

    return false
}

...

마지막으로 이전 단계에서 생성된 MasterPasword 인스턴스에서 Crypto 개체의 인스턴스를 가져오고 다음 루프에서 두 가지 작업을 수행합니다.

  1. DTO를 복호화한 후 이미 복호화된 비밀번호를 사용하여 PasswordEntry로 변환하고
  2. 새 마스터 비밀번호로 암호화될 데이터베이스에 PasswordEntry를 삽입합니다.

마지막으로 남은 일은 언어 코드를 데이터베이스에 저장하는 일입니다.

오늘은 이것으로 충분합니다. 튜토리얼이 벌써 길어졌네요 ?‍?.

두 번째 부분에서는 말씀드린 대로 Svelte로 제작된 프런트엔드 부분에 대해 자세히 설명하겠습니다.

참을성이 없다면 이미 말씀드린 대로 이 저장소에서 모든 코드를 찾을 수 있습니다.

2부에서 만나요. 즐거운 코딩 되셨나요?!

위 내용은 미니멀한 비밀번호 관리자 데스크탑 앱: Golang의 Wails 프레임워크 진출(1부)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.