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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
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.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.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.actionAbout_Qt.OnTriggered(this.on_actionAbout_Qt_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)
|
||||
}
|
||||
|
||||
func getDisplayName(qba string) string {
|
||||
if qba == "" {
|
||||
return "<empty>"
|
||||
func (this *MainWindow) onactionExport_database_as_zip_triggered() {
|
||||
|
||||
dbPath := qt.QFileDialog_GetOpenFileName2(this.Widget(), "Select bolt database...")
|
||||
if dbPath == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ret := strconv.Quote(qba)
|
||||
return ret[1 : len(ret)-1]
|
||||
zipPath := qt.QFileDialog_GetSaveFileName4(this.Widget(), "Save as...", "", "Zip files (*.zip)")
|
||||
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) {
|
||||
|
@ -269,17 +269,27 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>668</width>
|
||||
<height>29</height>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuFile">
|
||||
<property name="title">
|
||||
<string>&File</string>
|
||||
</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="actionOpen_database"/>
|
||||
<addaction name="actionOpen_database_as_read_only"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="menuConvert"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionExit"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuHelp">
|
||||
@ -407,6 +417,16 @@
|
||||
<string>Open database as read-only...</string>
|
||||
</property>
|
||||
</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>
|
||||
<layoutdefault spacing="6" margin="11"/>
|
||||
<resources>
|
||||
|
@ -36,6 +36,7 @@ type MainWindowUi struct {
|
||||
DeleteDataButton *qt.QPushButton
|
||||
menuBar *qt.QMenuBar
|
||||
menuFile *qt.QMenu
|
||||
menuConvert *qt.QMenu
|
||||
menuHelp *qt.QMenu
|
||||
menuView *qt.QMenu
|
||||
mainToolBar *qt.QToolBar
|
||||
@ -51,6 +52,8 @@ type MainWindowUi struct {
|
||||
actionNew_database *qt.QAction
|
||||
actionAdd_bucket *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.
|
||||
@ -110,6 +113,10 @@ func NewMainWindowUi() *MainWindowUi {
|
||||
|
||||
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.gridLayout = qt.NewQGridLayout(ui.centralWidget)
|
||||
@ -217,13 +224,20 @@ func NewMainWindowUi() *MainWindowUi {
|
||||
ui.MainWindow.SetCentralWidget(ui.centralWidget) // Set central widget
|
||||
|
||||
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.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.actionOpen_database)
|
||||
ui.menuFile.QWidget.AddAction(ui.actionOpen_database_as_read_only)
|
||||
ui.menuFile.AddSeparator()
|
||||
ui.menuFile.AddMenu(ui.menuConvert)
|
||||
ui.menuFile.AddSeparator()
|
||||
ui.menuFile.AddSeparator()
|
||||
ui.menuFile.QWidget.AddAction(ui.actionExit)
|
||||
|
||||
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.actionAdd_bucket.SetText(qt.QMainWindow_Tr("Add bucket..."))
|
||||
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.databaseTabWidget.SetTabText(ui.databaseTabWidget.IndexOf(ui.databasePropertiesTab), qt.QTabWidget_Tr("Database"))
|
||||
ui.databasePropertiesArea.SetPlainText(qt.QWidget_Tr("No selection"))
|
||||
@ -282,6 +298,7 @@ func (ui *MainWindowUi) Retranslate() {
|
||||
ui.AddDataButton.SetText(qt.QWidget_Tr("Add..."))
|
||||
ui.DeleteDataButton.SetText(qt.QWidget_Tr("Delete..."))
|
||||
ui.menuFile.SetTitle(qt.QMenuBar_Tr("&File"))
|
||||
ui.menuConvert.SetTitle(qt.QMenu_Tr("Convert"))
|
||||
ui.menuHelp.SetTitle(qt.QMenuBar_Tr("Help"))
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
// 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