refactor: Wszystkie pakiety przeniesione do internal
This commit is contained in:
		
							
								
								
									
										75
									
								
								internal/cfg/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								internal/cfg/config.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
package cfg
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Connection struct {
 | 
			
		||||
	Host   string `json:"Host,omitempty"`
 | 
			
		||||
	User   string `json:"User,omitempty"`
 | 
			
		||||
	Port   uint   `json:"Port,omitempty"`
 | 
			
		||||
	DbName string `json:"DbName,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	PassFile    string       `json:"Passfile"`
 | 
			
		||||
	PsqlExec    string       `json:"PsqlExec"`
 | 
			
		||||
	SqlDir      string       `json:"SqlDir"`
 | 
			
		||||
	Defaults    Connection   `json:"Defaults"`
 | 
			
		||||
	Connections []Connection `json:"Connections"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetConfig(params Parameters) (config Config, err error) {
 | 
			
		||||
	bytes, err := os.ReadFile(params.ConfigFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return config, fmt.Errorf("błąd odczytu pliku konfiguracji: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	err = json.Unmarshal(bytes, &config)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return config, fmt.Errorf("błąd parsowania pliku konfiguracji: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	if config.PassFile == "" && params.Passfile != "" {
 | 
			
		||||
		config.PassFile = params.Passfile
 | 
			
		||||
	}
 | 
			
		||||
	if config.PassFile == "" {
 | 
			
		||||
		return config, fmt.Errorf("nie podano Passfile (dodaj w konfiguracji lub użyj flag --passfile)")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if config.SqlDir == "" && params.SqlDir != "" {
 | 
			
		||||
		config.SqlDir = params.SqlDir
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = fixConnections(&config)
 | 
			
		||||
	return config, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func fixConnections(config *Config) error {
 | 
			
		||||
	def := config.Defaults
 | 
			
		||||
	for i := range config.Connections {
 | 
			
		||||
		con := &config.Connections[i]
 | 
			
		||||
 | 
			
		||||
		if con.DbName == "" {
 | 
			
		||||
			con.DbName = def.DbName
 | 
			
		||||
		}
 | 
			
		||||
		if con.User == "" {
 | 
			
		||||
			con.User = def.User
 | 
			
		||||
		}
 | 
			
		||||
		if con.Host == "" {
 | 
			
		||||
			con.Host = def.Host
 | 
			
		||||
		}
 | 
			
		||||
		if con.Port == 0 {
 | 
			
		||||
			if def.Port != 0 {
 | 
			
		||||
				con.Port = def.Port
 | 
			
		||||
			} else {
 | 
			
		||||
				con.Port = 5432 // domyślny dla PostgreSQL
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if con.DbName == "" || con.Host == "" || con.User == "" {
 | 
			
		||||
			return fmt.Errorf("opis połączenia z pozycji %d jest niekompletny: %#v", i+1, con)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										137
									
								
								internal/cfg/params.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								internal/cfg/params.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,137 @@
 | 
			
		||||
package cfg
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const MULTISQLPASS = "MULTISQLPASS" // nazwa ziennej środowiskowej z hasłem
 | 
			
		||||
 | 
			
		||||
type Parameters struct {
 | 
			
		||||
	ConfigFile string
 | 
			
		||||
	Passfile   string
 | 
			
		||||
	OutDir     string
 | 
			
		||||
	SqlDir     string
 | 
			
		||||
	LogFile    string
 | 
			
		||||
	Verbose    bool
 | 
			
		||||
	AskPass    bool
 | 
			
		||||
	Version    bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var params Parameters
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	flag.BoolVar(¶ms.Verbose, "verbose", false, "Dodatkowe komunkaty diagnostyczne")
 | 
			
		||||
	flag.StringVar(¶ms.ConfigFile, "config", "multisql.conf", "Plik konfiguracji")
 | 
			
		||||
	flag.StringVar(¶ms.Passfile, "passfile", "", "Plik pgpass z hasłami do baz")
 | 
			
		||||
	flag.StringVar(¶ms.OutDir, "outdir", "", "Katalog (istniejący i z prawem do zapisu), do którego generowane są wyniki")
 | 
			
		||||
	flag.StringVar(¶ms.SqlDir, "sqldir", "scripts", "Katalog, w którym znajdują się skrypty do uruchomienia")
 | 
			
		||||
	flag.StringVar(¶ms.LogFile, "log", "", "Plik, do którego zostanie dopisany log programu")
 | 
			
		||||
	flag.BoolVar(¶ms.AskPass, "P", false, "Pytaj o hasło. Jeśli nie podane wymaga się hasła w ")
 | 
			
		||||
	flag.BoolVar(¶ms.Version, "version", false, "Wypisuje wersję i kończy działanie")
 | 
			
		||||
 | 
			
		||||
	flag.Usage = printUsage
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
}
 | 
			
		||||
func GetParams() Parameters {
 | 
			
		||||
	return params
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func printUsage() {
 | 
			
		||||
	fmt.Fprintf(os.Stderr,
 | 
			
		||||
		`
 | 
			
		||||
multisql - uruchamia zestaw skryptów na skonfigurowanej liście baz danych.
 | 
			
		||||
 | 
			
		||||
Program, dla każdej z baz wskazanych w konfiguracji, uruchamia kolejno każdy
 | 
			
		||||
z plików sql wskazanych parametrem lub w pliku konfiguracji poprzez
 | 
			
		||||
uruchomienie narzędzia psql:
 | 
			
		||||
 | 
			
		||||
    psql -f skrypt.sql -w -L log.txt _szczegóły_połaczenia_ 2>stderr.txt  > stdout.txt
 | 
			
		||||
 | 
			
		||||
Skrypty mogą mieć dowolne rozszerzenia ale podkatalogi nie są obsługiwane.
 | 
			
		||||
Pliki wynikowe zapisywane są do katalogu wskazanego w outdir w strukturze o postaci
 | 
			
		||||
output:
 | 
			
		||||
 -> data-godzina-pid
 | 
			
		||||
    -> host-port-baza-user
 | 
			
		||||
	   ->  skrypt.sql
 | 
			
		||||
	       -> log.txt
 | 
			
		||||
	       -> stdout.txt
 | 
			
		||||
		   -> stderr.txt
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Użycie:
 | 
			
		||||
		multisql -outdir /tmp -sqldir /data/skrypty -passfile ./hasla
 | 
			
		||||
 | 
			
		||||
Użycie w trybie szyfrowania:
 | 
			
		||||
		multisql encrypt plik.wynikowy.zaszyfrowany plik.zródłowy
 | 
			
		||||
	lub
 | 
			
		||||
		multisql [-P] decrypt plik.źródłowy.zaszyfrowany [plik.wynikowy.jawny]
 | 
			
		||||
 | 
			
		||||
Opis flag:
 | 
			
		||||
`,
 | 
			
		||||
	)
 | 
			
		||||
	flag.PrintDefaults()
 | 
			
		||||
	fmt.Fprintf(os.Stderr,
 | 
			
		||||
		`
 | 
			
		||||
Plik passfile ma standardowy format .pgpass PostgreSQLa.
 | 
			
		||||
 | 
			
		||||
Plik multisql.conf ma format JSON, np:
 | 
			
		||||
> `,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	c := Config{
 | 
			
		||||
		PassFile: "mypgpass",
 | 
			
		||||
		PsqlExec: "/usr/local/bin/psql",
 | 
			
		||||
		SqlDir:   "/data/skrypty-sql",
 | 
			
		||||
		Defaults: Connection{
 | 
			
		||||
			Port:   5433,
 | 
			
		||||
			User:   "myapp",
 | 
			
		||||
			DbName: "moja-db",
 | 
			
		||||
		},
 | 
			
		||||
		Connections: []Connection{
 | 
			
		||||
			{
 | 
			
		||||
				Host: "10.20.30.01",
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				Host:   "10.20.30.02",
 | 
			
		||||
				DbName: "innadb",
 | 
			
		||||
				User:   "innyuser",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b, _ := json.Marshal(&c)
 | 
			
		||||
	var out bytes.Buffer
 | 
			
		||||
	_ = json.Indent(&out, b, "> ", "\t")
 | 
			
		||||
	_, _ = out.WriteTo(os.Stderr)
 | 
			
		||||
 | 
			
		||||
	fmt.Fprintf(os.Stderr, `
 | 
			
		||||
 | 
			
		||||
PsqlExec jest opcjonalny - jesli nie zostanie podany wyszukuje się programu psql.exe w PATH.
 | 
			
		||||
 | 
			
		||||
Każde połączenie w tablicy Connections może zawierać Host, Port, User i DbName.
 | 
			
		||||
Brakujące wartości są uzupełniane z sekcji Defaults (która ma taki sam
 | 
			
		||||
format jak obiekt w Connections).
 | 
			
		||||
 | 
			
		||||
Parametry mają pierwszeństwo przed wartościami w konfiguracji.
 | 
			
		||||
 | 
			
		||||
Format pliku passfile jest następujący:
 | 
			
		||||
 | 
			
		||||
    hostname:port:database:username:password
 | 
			
		||||
 | 
			
		||||
W szczegółach opisano go na https://www.postgresql.org/docs/current/libpq-pgpass.html.
 | 
			
		||||
 | 
			
		||||
Aktualnie obsługiwana jest jedynie zaszyforowana postać pliku. Przed użyciem należy
 | 
			
		||||
plik zaszyfrować:
 | 
			
		||||
 | 
			
		||||
    multsql encrypt pgpass.encrypted  pgpass
 | 
			
		||||
 | 
			
		||||
Przy użyciu (odszyfrowaniu) pliku, hasło jest pobierane ze zmiennej
 | 
			
		||||
środowiskowej %s lub z klawiatury, jeśli użyto opcji -P.
 | 
			
		||||
 | 
			
		||||
`, MULTISQLPASS)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										132
									
								
								internal/mgr/mgr.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								internal/mgr/mgr.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,132 @@
 | 
			
		||||
package mgr
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"baal.ar76.eu/x/pub/multisql/internal/cfg"
 | 
			
		||||
	"baal.ar76.eu/x/pub/multisql/internal/pass"
 | 
			
		||||
	"baal.ar76.eu/x/pub/multisql/internal/psql"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Manager struct {
 | 
			
		||||
	config  cfg.Config
 | 
			
		||||
	runDir  string
 | 
			
		||||
	verbose bool
 | 
			
		||||
	askPass bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Create(verbose bool, askPass bool, outdir string, config cfg.Config) (Manager, error) {
 | 
			
		||||
	runDir := createRunDir(outdir)
 | 
			
		||||
 | 
			
		||||
	manager := Manager{
 | 
			
		||||
		config:  config,
 | 
			
		||||
		runDir:  runDir,
 | 
			
		||||
		verbose: verbose,
 | 
			
		||||
		askPass: askPass,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return manager, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func createRunDir(out string) string {
 | 
			
		||||
	t := time.Now()
 | 
			
		||||
	dataPart := t.Format("2006-01-02T15_04_05")
 | 
			
		||||
	newDir := fmt.Sprintf("%s-%d", dataPart, os.Getpid())
 | 
			
		||||
	dir := path.Join(out, newDir)
 | 
			
		||||
	return dir
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (self Manager) GetScripts() ([]string, error) {
 | 
			
		||||
	entries, err := os.ReadDir(self.config.SqlDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("błąd wczytywania skryptów: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	scripts := make([]string, 0, len(entries))
 | 
			
		||||
	for _, e := range entries {
 | 
			
		||||
		if e.IsDir() {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		scrpt := path.Join(self.config.SqlDir, e.Name())
 | 
			
		||||
		_, err := os.Stat(scrpt)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Printf("Skrypt zostanie pominięty z powodu błedu: %v\n", err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		scripts = append(scripts, scrpt)
 | 
			
		||||
	}
 | 
			
		||||
	return scripts, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (self Manager) Run() error {
 | 
			
		||||
	err := os.Mkdir(self.runDir, 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("błąd tworzenia katalogu z wynikami: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	scripts, err := self.GetScripts()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var passdb *pass.PassDb
 | 
			
		||||
 | 
			
		||||
	if self.config.PassFile != "" {
 | 
			
		||||
		password := pass.GetMasterPass(self.askPass)
 | 
			
		||||
		passdb, err = pass.Load(self.config.PassFile, password)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var group sync.WaitGroup
 | 
			
		||||
 | 
			
		||||
	for _, con := range self.config.Connections {
 | 
			
		||||
		dbDir, err := self.createDirPerCon(con)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Printf("Problem z utworzeniem katalogu wynikow dla połaczenie: %v", dbDir)
 | 
			
		||||
			log.Printf("Pomijam połaczenie")
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		group.Add(1)
 | 
			
		||||
		stream := make(chan psql.Result)
 | 
			
		||||
		go self.Logger(&group, stream, con)
 | 
			
		||||
 | 
			
		||||
		sql := psql.Create(
 | 
			
		||||
			passdb,
 | 
			
		||||
			dbDir,
 | 
			
		||||
			scripts,
 | 
			
		||||
			con,
 | 
			
		||||
			self.config.PsqlExec,
 | 
			
		||||
			self.verbose,
 | 
			
		||||
		)
 | 
			
		||||
		go sql.Exec(stream)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	group.Wait()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (self Manager) createDirPerCon(con cfg.Connection) (string, error) {
 | 
			
		||||
	dbDir := fmt.Sprintf("%s_%d_%s_%s", con.Host, con.Port, con.DbName, con.User)
 | 
			
		||||
	dir := path.Join(self.runDir, dbDir)
 | 
			
		||||
	err := os.Mkdir(dir, 0o755)
 | 
			
		||||
	return dir, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (self Manager) Logger(group *sync.WaitGroup, stream <-chan psql.Result, con cfg.Connection) {
 | 
			
		||||
	for event := range stream {
 | 
			
		||||
		if event.Err != nil {
 | 
			
		||||
			scr := path.Base(event.Script)
 | 
			
		||||
			log.Printf("%s:%d:%s:%s Skrypt: %s Błąd: %v\n", con.Host, con.Port, con.DbName, con.User, scr, event.Err)
 | 
			
		||||
		} else if self.verbose {
 | 
			
		||||
			scr := path.Base(event.Script)
 | 
			
		||||
			log.Printf("%s:%d:%s:%s Skrypt %s zakończony poprawnie\n", con.Host, con.Port, con.DbName, con.User, scr)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	group.Done()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								internal/pass/crypt.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								internal/pass/crypt.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
package pass
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"filippo.io/age"
 | 
			
		||||
	"filippo.io/age/armor"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func DecryptFile(password []byte, file string) ([]byte, error) {
 | 
			
		||||
	identity, err := age.NewScryptIdentity(string(password))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("błąd przetwarzania hasła: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	fd, err := os.Open(file)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("błąd otwarcia pliku: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer fd.Close()
 | 
			
		||||
 | 
			
		||||
	in := armor.NewReader(fd)
 | 
			
		||||
 | 
			
		||||
	r, err := age.Decrypt(in, identity)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("błąd deszyfrowania #01: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	out := &bytes.Buffer{}
 | 
			
		||||
	if _, err := io.Copy(out, r); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("błąd deszyfrowania #02: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return out.Bytes(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func EncryptFile(password []byte, file string, data []byte) error {
 | 
			
		||||
	recipient, err := age.NewScryptRecipient(string(password))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("błąd przetwarzania hasła: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	fd, err := os.OpenFile(file, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0o600)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("błąd tworzenia pliku: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer fd.Close()
 | 
			
		||||
 | 
			
		||||
	out := armor.NewWriter(fd) // armor
 | 
			
		||||
	defer out.Close()
 | 
			
		||||
 | 
			
		||||
	w, err := age.Encrypt(out, recipient)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("błąd szyfrowania #01: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	_, err = w.Write(data)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("błąd szyfrowania #02: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	err = w.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("błąd szyfrowania #03: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										80
									
								
								internal/pass/pass.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								internal/pass/pass.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
package pass
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type pgpassrow struct {
 | 
			
		||||
	hostname string
 | 
			
		||||
	port     string
 | 
			
		||||
	database string
 | 
			
		||||
	username string
 | 
			
		||||
	password string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PassDb struct {
 | 
			
		||||
	db []pgpassrow
 | 
			
		||||
	cache map[string]*pgpassrow
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Load(file string, master []byte) (*PassDb, error) {
 | 
			
		||||
 | 
			
		||||
	data, err := DecryptFile(master, file)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lines := bytes.Split(data, []byte{'\n'})
 | 
			
		||||
 | 
			
		||||
	var rows []pgpassrow
 | 
			
		||||
 | 
			
		||||
	for i, line := range lines {
 | 
			
		||||
		line = bytes.TrimRight(line, "\n\r \t")
 | 
			
		||||
		if len(line) == 0 || line[0] == byte('#') {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		fields := bytes.Split(line, []byte{':'})
 | 
			
		||||
		if len(fields) != 5 {
 | 
			
		||||
			return nil, fmt.Errorf("błąd w pliku hasłeł %s:%d: niewłaściwa liczba pól", file, i)
 | 
			
		||||
		}
 | 
			
		||||
		rows = append(rows, pgpassrow{
 | 
			
		||||
			hostname: string(fields[0]),
 | 
			
		||||
			port:     string(fields[1]),
 | 
			
		||||
			database: string(fields[2]),
 | 
			
		||||
			username: string(fields[3]),
 | 
			
		||||
			password: string(fields[4]),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &PassDb{
 | 
			
		||||
		db: rows,
 | 
			
		||||
		cache: make(map[string]*pgpassrow),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func match(value string, pattern string) bool {
 | 
			
		||||
	return pattern == "*" || pattern == value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (self *PassDb) FindPassword(host string, port uint, database string, user string) string {
 | 
			
		||||
	myport := fmt.Sprintf("%d", port)
 | 
			
		||||
	key := fmt.Sprintf("%s:%s:%s:%s", host, myport, database, user)
 | 
			
		||||
	cached, ok := self.cache[key]
 | 
			
		||||
	if ok {
 | 
			
		||||
		return cached.password
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// pierwszy pasujący do wzorca
 | 
			
		||||
	for i := range self.db {
 | 
			
		||||
		if match(host, self.db[i].hostname) &&
 | 
			
		||||
			match(myport, self.db[i].port) &&
 | 
			
		||||
			match(database, self.db[i].database) &&
 | 
			
		||||
			match(user, self.db[i].username) {
 | 
			
		||||
 | 
			
		||||
				self.cache[key] = &self.db[i]
 | 
			
		||||
				return self.db[i].password
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								internal/pass/term.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								internal/pass/term.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
package pass
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"baal.ar76.eu/x/pub/multisql/internal/cfg"
 | 
			
		||||
	"golang.org/x/term"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetMasterPass(askPass bool) []byte {
 | 
			
		||||
	if askPass {
 | 
			
		||||
		res, err := termGetPassword("Podaj hasło")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatalln("Nie udało się wczytać hasła z konsoli")
 | 
			
		||||
		}
 | 
			
		||||
		return res
 | 
			
		||||
	}
 | 
			
		||||
	password := os.Getenv(cfg.MULTISQLPASS)
 | 
			
		||||
 | 
			
		||||
	if password != "" {
 | 
			
		||||
		return []byte(password)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Fatalf("Nieustalone hasło. Użyj flagi -P lub ustaw hasło w zmiennej %s", cfg.MULTISQLPASS)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func EnterMasterPass() []byte {
 | 
			
		||||
	p1, err := termGetPassword("Podaj nowe hasło (min 8 znaków)")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Błąd wczytywania hasła: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	if len(p1) < 8 {
 | 
			
		||||
		log.Fatalf("podano zbyt krótkie hasło")
 | 
			
		||||
	}
 | 
			
		||||
	p2, err := termGetPassword("Powtórz nowe hasło")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Błąd wczytywania hasła: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	if !bytes.Equal(p1, p2) {
 | 
			
		||||
		log.Fatalf("podane hasła są różne")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return p1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func termGetPassword(prompt string) ([]byte, error) {
 | 
			
		||||
	fmt.Fprintf(os.Stderr, "%s: ", prompt)
 | 
			
		||||
	passwd, err := term.ReadPassword(int(os.Stdin.Fd()))
 | 
			
		||||
	fmt.Fprintln(os.Stderr)
 | 
			
		||||
	return passwd, err
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										121
									
								
								internal/psql/psql.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								internal/psql/psql.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
package psql
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"runtime"
 | 
			
		||||
 | 
			
		||||
	"baal.ar76.eu/x/pub/multisql/internal/cfg"
 | 
			
		||||
	"baal.ar76.eu/x/pub/multisql/internal/pass"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Instance struct {
 | 
			
		||||
	outdir  string
 | 
			
		||||
	scripts []string
 | 
			
		||||
	pasdb   *pass.PassDb
 | 
			
		||||
	psqlExe string
 | 
			
		||||
	conn    cfg.Connection
 | 
			
		||||
	verbose bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Result struct {
 | 
			
		||||
	Script string
 | 
			
		||||
	Err    error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Create(passdb *pass.PassDb, outdir string, scripts []string, conn cfg.Connection, psqlExe string, verbose bool) (instnace Instance) {
 | 
			
		||||
 | 
			
		||||
	instnace = Instance{
 | 
			
		||||
		outdir:  outdir,
 | 
			
		||||
		scripts: scripts,
 | 
			
		||||
		pasdb:   passdb,
 | 
			
		||||
		conn:    conn,
 | 
			
		||||
		psqlExe: psqlExe,
 | 
			
		||||
		verbose: verbose,
 | 
			
		||||
	}
 | 
			
		||||
	return instnace
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (self Instance) Exec(result chan<- Result) {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	connString := fmt.Sprintf("host=%s port=%d dbname=%s user=%s", self.conn.Host, self.conn.Port, self.conn.DbName, self.conn.User)
 | 
			
		||||
 | 
			
		||||
	for _, scr := range self.scripts {
 | 
			
		||||
		srcName := path.Base(scr)
 | 
			
		||||
		outdir := path.Join(self.outdir, srcName)
 | 
			
		||||
		err := os.Mkdir(outdir, 0o775)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			result <- Result{
 | 
			
		||||
				Script: scr,
 | 
			
		||||
				Err:    err,
 | 
			
		||||
			}
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		password := self.pasdb.FindPassword(self.conn.Host, self.conn.Port, self.conn.DbName, self.conn.User)
 | 
			
		||||
		err = self.cmdPsql(outdir, connString, scr, password)
 | 
			
		||||
		result <- Result{
 | 
			
		||||
			Script: scr,
 | 
			
		||||
			Err:    err,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	close(result)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (self Instance) cmdPsql(outdir string, connString string, script string, password string) error {
 | 
			
		||||
	absScript, err := filepath.Abs(script)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("problem ze zbudowaniem ściezki do skryptu: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	psqlExe := "psql"
 | 
			
		||||
	if runtime.GOOS == "windows" {
 | 
			
		||||
		psqlExe = "psql.exe"
 | 
			
		||||
	}
 | 
			
		||||
	if self.psqlExe != "" {
 | 
			
		||||
		psqlExe = self.psqlExe
 | 
			
		||||
	}
 | 
			
		||||
	stdoutFile := path.Join(outdir, "stdout.txt")
 | 
			
		||||
	stderrFile := path.Join(outdir, "stderr.txt")
 | 
			
		||||
	logFile := "log.txt"
 | 
			
		||||
 | 
			
		||||
	fhOut, err := os.Create(stdoutFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Problem utworzenie pliku wyników: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer fhOut.Close()
 | 
			
		||||
 | 
			
		||||
	fhErr, err := os.Create(stderrFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Problem utworzenie pliku błędów: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer fhErr.Close()
 | 
			
		||||
 | 
			
		||||
	cmd := exec.Command(psqlExe,
 | 
			
		||||
		"-w",
 | 
			
		||||
		"-f", absScript,
 | 
			
		||||
		"-L", logFile,
 | 
			
		||||
		connString,
 | 
			
		||||
	)
 | 
			
		||||
	cmd.Stderr = fhErr
 | 
			
		||||
	cmd.Stdout = fhOut
 | 
			
		||||
	cmd.Dir = outdir
 | 
			
		||||
 | 
			
		||||
	if password != "" {
 | 
			
		||||
		env := os.Environ()
 | 
			
		||||
		env = append(env, "PGPASSWORD="+password)
 | 
			
		||||
		cmd.Env = env
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if self.verbose {
 | 
			
		||||
		log.Printf("Uruchamiam %s", cmd.String())
 | 
			
		||||
	}
 | 
			
		||||
	err = cmd.Run()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Błąd uruchamiania psql: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user