add zip roundtrip conversion feature
This commit is contained in:
parent
cbfc038839
commit
9ac26467c0
167
export.go
Normal file
167
export.go
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Bolt_ExportDatabaseToZip(dbpath, zippath string) error {
|
||||||
|
db, err := Bolt_Open(true, dbpath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error opening database: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
fh, err := os.OpenFile(zippath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error opening output file: %w", err)
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
zw := zip.NewWriter(fh)
|
||||||
|
|
||||||
|
// Filenames in zip files cannot contain `/` characters. Mangle it
|
||||||
|
safename := func(n string) string {
|
||||||
|
return strings.ReplaceAll(string(n), `/`, `__`)
|
||||||
|
}
|
||||||
|
|
||||||
|
var process func(currentPath []string) error
|
||||||
|
process = func(currentPath []string) error {
|
||||||
|
return Bolt_ListBuckets(db, currentPath, func(bucket string) error {
|
||||||
|
|
||||||
|
// Create entry for our own bucket
|
||||||
|
|
||||||
|
ourBucket := zip.FileHeader{
|
||||||
|
Name: path.Join(path.Join(Apply(currentPath, safename)...), safename(bucket)) + `/`, // Trailing slash = directory
|
||||||
|
}
|
||||||
|
ourBucket.SetMode(fs.ModeDir | 0755)
|
||||||
|
_, err := zw.CreateHeader(&ourBucket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Child pathspec
|
||||||
|
|
||||||
|
childPath := CopySliceAdd(currentPath, bucket)
|
||||||
|
|
||||||
|
// Create file entries for all non-bucket children
|
||||||
|
|
||||||
|
err = Bolt_ListItems(db, childPath, func(li ListItemInfo) error {
|
||||||
|
fileItem := zip.FileHeader{
|
||||||
|
Name: path.Join(path.Join(Apply(childPath, safename)...), safename(string(li.Name))),
|
||||||
|
}
|
||||||
|
fileItem.SetMode(0644)
|
||||||
|
fileW, err := zw.CreateHeader(&fileItem)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
buff, err := Bolt_GetItem(db, childPath, []byte(li.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.CopyN(fileW, bytes.NewReader(buff), li.DataLen)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse for all bucket-type children
|
||||||
|
|
||||||
|
process(childPath)
|
||||||
|
|
||||||
|
// Done
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = process([]string{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = zw.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = zw.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fh.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Bolt_ImportZipToDatabase(dbpath, zippath string) error {
|
||||||
|
|
||||||
|
db, err := Bolt_Open(false, dbpath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error opening target database: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
fh, err := os.OpenFile(zippath, os.O_RDONLY, 0400)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error opening input archive: %w", err)
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
fstat, err := fh.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zr, err := zip.NewReader(fh, fstat.Size())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Reading zip file format: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, zf := range zr.File {
|
||||||
|
if strings.HasSuffix(zf.Name, `/`) || (zf.Mode()&fs.ModeDir) != 0 {
|
||||||
|
// Bucket
|
||||||
|
bucketPath := strings.Split(strings.TrimSuffix(zf.Name, `/`), `/`)
|
||||||
|
err = Bolt_CreateBucket(db, bucketPath[0:len(bucketPath)-1], bucketPath[len(bucketPath)-1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Creating bucket %q: %w", zf.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Object
|
||||||
|
objectPath := strings.Split(zf.Name, `/`)
|
||||||
|
|
||||||
|
rc, err := zf.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := io.ReadAll(rc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = Bolt_SetItem(db, objectPath[0:len(objectPath)-1], []byte(objectPath[len(objectPath)-1]), content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rc.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done
|
||||||
|
return nil
|
||||||
|
}
|
@ -2,8 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
qt "github.com/mappu/miqt/qt6"
|
qt "github.com/mappu/miqt/qt6"
|
||||||
@ -40,6 +40,8 @@ func NewMainWindow() *MainWindow {
|
|||||||
this.ui.actionNew_database.OnTriggered(this.on_actionNew_database_triggered)
|
this.ui.actionNew_database.OnTriggered(this.on_actionNew_database_triggered)
|
||||||
this.ui.actionOpen_database.OnTriggered(this.on_actionOpen_database_triggered)
|
this.ui.actionOpen_database.OnTriggered(this.on_actionOpen_database_triggered)
|
||||||
this.ui.actionOpen_database_as_read_only.OnTriggered(this.on_actionOpen_database_as_read_only_triggered)
|
this.ui.actionOpen_database_as_read_only.OnTriggered(this.on_actionOpen_database_as_read_only_triggered)
|
||||||
|
this.ui.actionExport_database_as_zip.OnTriggered(this.onactionExport_database_as_zip_triggered)
|
||||||
|
this.ui.actionCreate_database_from_zip.OnTriggered(this.onactionCreate_database_from_zip_triggered)
|
||||||
this.ui.actionExit.OnTriggered(this.on_actionExit_triggered)
|
this.ui.actionExit.OnTriggered(this.on_actionExit_triggered)
|
||||||
this.ui.actionAbout_Qt.OnTriggered(this.on_actionAbout_Qt_triggered)
|
this.ui.actionAbout_Qt.OnTriggered(this.on_actionAbout_Qt_triggered)
|
||||||
this.ui.actionAbout_qbolt.OnTriggered(this.on_actionAbout_qbolt_triggered)
|
this.ui.actionAbout_qbolt.OnTriggered(this.on_actionAbout_qbolt_triggered)
|
||||||
@ -134,13 +136,53 @@ func (this *MainWindow) openDatabase(file string, readOnly bool) {
|
|||||||
this.ui.bucketTree.ExpandItem(top)
|
this.ui.bucketTree.ExpandItem(top)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDisplayName(qba string) string {
|
func (this *MainWindow) onactionExport_database_as_zip_triggered() {
|
||||||
if qba == "" {
|
|
||||||
return "<empty>"
|
dbPath := qt.QFileDialog_GetOpenFileName2(this.Widget(), "Select bolt database...")
|
||||||
|
if dbPath == "" {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ret := strconv.Quote(qba)
|
zipPath := qt.QFileDialog_GetSaveFileName4(this.Widget(), "Save as...", "", "Zip files (*.zip)")
|
||||||
return ret[1 : len(ret)-1]
|
if zipPath == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Bolt_ExportDatabaseToZip(dbPath, zipPath)
|
||||||
|
if err != nil {
|
||||||
|
this.alert(fmt.Sprintf("Error exporting database as zip: %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.alert("Exported as zip successfully.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *MainWindow) onactionCreate_database_from_zip_triggered() {
|
||||||
|
|
||||||
|
zipPath := qt.QFileDialog_GetOpenFileName4(this.Widget(), "Select zip archive...", "", "Zip files (*.zip)")
|
||||||
|
if zipPath == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := qt.QFileDialog_GetSaveFileName2(this.Widget(), "Save as...")
|
||||||
|
if dbPath == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qt popped up a message saying 'will overwrite existing'
|
||||||
|
// Make that true
|
||||||
|
if err := os.Remove(dbPath); err != nil && !os.IsNotExist(err) {
|
||||||
|
this.alert(fmt.Sprintf("Error removing existing database for overwrite: %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Bolt_ImportZipToDatabase(dbPath, zipPath)
|
||||||
|
if err != nil {
|
||||||
|
this.alert(fmt.Sprintf("Error importing database from zip %q: %s", zipPath, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.alert("Imported zip to database successfully.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *MainWindow) refreshBucketTree(itm *qt.QTreeWidgetItem) {
|
func (this *MainWindow) refreshBucketTree(itm *qt.QTreeWidgetItem) {
|
||||||
|
@ -269,17 +269,27 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>668</width>
|
<width>668</width>
|
||||||
<height>29</height>
|
<height>22</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QMenu" name="menuFile">
|
<widget class="QMenu" name="menuFile">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>&File</string>
|
<string>&File</string>
|
||||||
</property>
|
</property>
|
||||||
|
<widget class="QMenu" name="menuConvert">
|
||||||
|
<property name="title">
|
||||||
|
<string>Convert</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="actionExport_database_as_zip"/>
|
||||||
|
<addaction name="actionCreate_database_from_zip"/>
|
||||||
|
</widget>
|
||||||
<addaction name="actionNew_database"/>
|
<addaction name="actionNew_database"/>
|
||||||
<addaction name="actionOpen_database"/>
|
<addaction name="actionOpen_database"/>
|
||||||
<addaction name="actionOpen_database_as_read_only"/>
|
<addaction name="actionOpen_database_as_read_only"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
|
<addaction name="menuConvert"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
<addaction name="actionExit"/>
|
<addaction name="actionExit"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menuHelp">
|
<widget class="QMenu" name="menuHelp">
|
||||||
@ -407,6 +417,16 @@
|
|||||||
<string>Open database as read-only...</string>
|
<string>Open database as read-only...</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionExport_database_as_zip">
|
||||||
|
<property name="text">
|
||||||
|
<string>Export database as zip</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionCreate_database_from_zip">
|
||||||
|
<property name="text">
|
||||||
|
<string>Create database from zip</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<layoutdefault spacing="6" margin="11"/>
|
<layoutdefault spacing="6" margin="11"/>
|
||||||
<resources>
|
<resources>
|
||||||
|
@ -36,6 +36,7 @@ type MainWindowUi struct {
|
|||||||
DeleteDataButton *qt.QPushButton
|
DeleteDataButton *qt.QPushButton
|
||||||
menuBar *qt.QMenuBar
|
menuBar *qt.QMenuBar
|
||||||
menuFile *qt.QMenu
|
menuFile *qt.QMenu
|
||||||
|
menuConvert *qt.QMenu
|
||||||
menuHelp *qt.QMenu
|
menuHelp *qt.QMenu
|
||||||
menuView *qt.QMenu
|
menuView *qt.QMenu
|
||||||
mainToolBar *qt.QToolBar
|
mainToolBar *qt.QToolBar
|
||||||
@ -51,6 +52,8 @@ type MainWindowUi struct {
|
|||||||
actionNew_database *qt.QAction
|
actionNew_database *qt.QAction
|
||||||
actionAdd_bucket *qt.QAction
|
actionAdd_bucket *qt.QAction
|
||||||
actionOpen_database_as_read_only *qt.QAction
|
actionOpen_database_as_read_only *qt.QAction
|
||||||
|
actionExport_database_as_zip *qt.QAction
|
||||||
|
actionCreate_database_from_zip *qt.QAction
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMainWindowUi creates all Qt widget classes for MainWindow.
|
// NewMainWindowUi creates all Qt widget classes for MainWindow.
|
||||||
@ -110,6 +113,10 @@ func NewMainWindowUi() *MainWindowUi {
|
|||||||
|
|
||||||
ui.actionOpen_database_as_read_only = qt.NewQAction()
|
ui.actionOpen_database_as_read_only = qt.NewQAction()
|
||||||
|
|
||||||
|
ui.actionExport_database_as_zip = qt.NewQAction()
|
||||||
|
|
||||||
|
ui.actionCreate_database_from_zip = qt.NewQAction()
|
||||||
|
|
||||||
ui.centralWidget = qt.NewQWidget(ui.MainWindow.QWidget)
|
ui.centralWidget = qt.NewQWidget(ui.MainWindow.QWidget)
|
||||||
|
|
||||||
ui.gridLayout = qt.NewQGridLayout(ui.centralWidget)
|
ui.gridLayout = qt.NewQGridLayout(ui.centralWidget)
|
||||||
@ -217,13 +224,20 @@ func NewMainWindowUi() *MainWindowUi {
|
|||||||
ui.MainWindow.SetCentralWidget(ui.centralWidget) // Set central widget
|
ui.MainWindow.SetCentralWidget(ui.centralWidget) // Set central widget
|
||||||
|
|
||||||
ui.menuBar = qt.NewQMenuBar(ui.MainWindow.QWidget)
|
ui.menuBar = qt.NewQMenuBar(ui.MainWindow.QWidget)
|
||||||
ui.menuBar.Resize(668, 29)
|
ui.menuBar.Resize(668, 22)
|
||||||
|
|
||||||
ui.menuFile = qt.NewQMenu(ui.menuBar.QWidget)
|
ui.menuFile = qt.NewQMenu(ui.menuBar.QWidget)
|
||||||
|
|
||||||
|
ui.menuConvert = qt.NewQMenu(ui.menuFile.QWidget)
|
||||||
|
ui.menuConvert.QWidget.AddAction(ui.actionExport_database_as_zip)
|
||||||
|
ui.menuConvert.QWidget.AddAction(ui.actionCreate_database_from_zip)
|
||||||
ui.menuFile.QWidget.AddAction(ui.actionNew_database)
|
ui.menuFile.QWidget.AddAction(ui.actionNew_database)
|
||||||
ui.menuFile.QWidget.AddAction(ui.actionOpen_database)
|
ui.menuFile.QWidget.AddAction(ui.actionOpen_database)
|
||||||
ui.menuFile.QWidget.AddAction(ui.actionOpen_database_as_read_only)
|
ui.menuFile.QWidget.AddAction(ui.actionOpen_database_as_read_only)
|
||||||
ui.menuFile.AddSeparator()
|
ui.menuFile.AddSeparator()
|
||||||
|
ui.menuFile.AddMenu(ui.menuConvert)
|
||||||
|
ui.menuFile.AddSeparator()
|
||||||
|
ui.menuFile.AddSeparator()
|
||||||
ui.menuFile.QWidget.AddAction(ui.actionExit)
|
ui.menuFile.QWidget.AddAction(ui.actionExit)
|
||||||
|
|
||||||
ui.menuHelp = qt.NewQMenu(ui.menuBar.QWidget)
|
ui.menuHelp = qt.NewQMenu(ui.menuBar.QWidget)
|
||||||
@ -272,6 +286,8 @@ func (ui *MainWindowUi) Retranslate() {
|
|||||||
ui.actionNew_database.SetText(qt.QMainWindow_Tr("&New database..."))
|
ui.actionNew_database.SetText(qt.QMainWindow_Tr("&New database..."))
|
||||||
ui.actionAdd_bucket.SetText(qt.QMainWindow_Tr("Add bucket..."))
|
ui.actionAdd_bucket.SetText(qt.QMainWindow_Tr("Add bucket..."))
|
||||||
ui.actionOpen_database_as_read_only.SetText(qt.QMainWindow_Tr("Open database as read-only..."))
|
ui.actionOpen_database_as_read_only.SetText(qt.QMainWindow_Tr("Open database as read-only..."))
|
||||||
|
ui.actionExport_database_as_zip.SetText(qt.QMainWindow_Tr("Export database as zip"))
|
||||||
|
ui.actionCreate_database_from_zip.SetText(qt.QMainWindow_Tr("Create database from zip"))
|
||||||
ui.bucketTree.HeaderItem().SetText(0, qt.QTreeWidget_Tr("Bucket"))
|
ui.bucketTree.HeaderItem().SetText(0, qt.QTreeWidget_Tr("Bucket"))
|
||||||
ui.databaseTabWidget.SetTabText(ui.databaseTabWidget.IndexOf(ui.databasePropertiesTab), qt.QTabWidget_Tr("Database"))
|
ui.databaseTabWidget.SetTabText(ui.databaseTabWidget.IndexOf(ui.databasePropertiesTab), qt.QTabWidget_Tr("Database"))
|
||||||
ui.databasePropertiesArea.SetPlainText(qt.QWidget_Tr("No selection"))
|
ui.databasePropertiesArea.SetPlainText(qt.QWidget_Tr("No selection"))
|
||||||
@ -282,6 +298,7 @@ func (ui *MainWindowUi) Retranslate() {
|
|||||||
ui.AddDataButton.SetText(qt.QWidget_Tr("Add..."))
|
ui.AddDataButton.SetText(qt.QWidget_Tr("Add..."))
|
||||||
ui.DeleteDataButton.SetText(qt.QWidget_Tr("Delete..."))
|
ui.DeleteDataButton.SetText(qt.QWidget_Tr("Delete..."))
|
||||||
ui.menuFile.SetTitle(qt.QMenuBar_Tr("&File"))
|
ui.menuFile.SetTitle(qt.QMenuBar_Tr("&File"))
|
||||||
|
ui.menuConvert.SetTitle(qt.QMenu_Tr("Convert"))
|
||||||
ui.menuHelp.SetTitle(qt.QMenuBar_Tr("Help"))
|
ui.menuHelp.SetTitle(qt.QMenuBar_Tr("Help"))
|
||||||
ui.menuView.SetTitle(qt.QMenuBar_Tr("&View"))
|
ui.menuView.SetTitle(qt.QMenuBar_Tr("&View"))
|
||||||
}
|
}
|
||||||
|
18
util.go
18
util.go
@ -20,3 +20,21 @@ func ReverseSlice[S ~[]E, E any](s S) {
|
|||||||
s[i], s[j] = s[j], s[i]
|
s[i], s[j] = s[j], s[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CopySliceAdd copies a slice and adds one more thing on the end.
|
||||||
|
func CopySliceAdd[T comparable](base []T, and T) []T {
|
||||||
|
ret := make([]T, len(base)+1)
|
||||||
|
copy(ret, base)
|
||||||
|
ret[len(ret)-1] = and
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply creates a new slice where every element of the input arr is transformed.
|
||||||
|
func Apply[T comparable](arr []T, transform func(T) T) []T {
|
||||||
|
ret := make([]T, len(arr))
|
||||||
|
for i := 0; i < len(arr); i++ {
|
||||||
|
ret[i] = transform(arr[i])
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user