2023-12-04 10:06:41 +00:00
//go:build linux
2020-05-15 09:39:16 +00:00
package main
import (
"fmt"
2022-05-18 09:05:01 +00:00
"math"
2020-05-15 09:39:16 +00:00
"os"
2021-02-10 08:23:33 +00:00
"os/exec"
"os/user"
2020-11-30 09:37:17 +00:00
"path/filepath"
2021-02-10 08:23:33 +00:00
"strconv"
2021-02-25 10:20:36 +00:00
"strings"
2023-12-04 10:06:41 +00:00
"syscall"
2021-05-17 09:43:53 +00:00
"unicode"
2020-11-30 09:37:17 +00:00
"github.com/AlecAivazis/survey/v2"
2022-05-18 09:05:01 +00:00
"github.com/pbnjay/memory"
2020-05-15 09:39:16 +00:00
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
2022-10-13 10:28:24 +00:00
"github.com/crowdsecurity/crowdsec/pkg/metabase"
2023-07-27 15:02:20 +00:00
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
2020-05-15 09:39:16 +00:00
)
var (
2020-11-30 09:37:17 +00:00
metabaseUser = "crowdsec@crowdsec.net"
metabasePassword string
metabaseDbPath string
metabaseConfigPath string
metabaseConfigFolder = "metabase/"
metabaseConfigFile = "metabase.yaml"
2023-07-25 13:21:25 +00:00
metabaseImage = "metabase/metabase:v0.46.6.1"
2020-05-15 09:39:16 +00:00
/**/
metabaseListenAddress = "127.0.0.1"
metabaseListenPort = "3000"
2021-07-01 16:15:22 +00:00
metabaseContainerID = "crowdsec-metabase"
2021-02-10 08:23:33 +00:00
crowdsecGroup = "crowdsec"
2020-11-30 09:37:17 +00:00
forceYes bool
2023-11-07 14:07:36 +00:00
// information needed to set up a random password on user's behalf
2020-05-15 09:39:16 +00:00
)
2023-12-11 09:32:54 +00:00
type cliDashboard struct { }
func NewCLIDashboard ( ) * cliDashboard {
return & cliDashboard { }
}
func ( cli cliDashboard ) NewCommand ( ) * cobra . Command {
2020-05-15 09:39:16 +00:00
/* ---- UPDATE COMMAND */
2023-12-11 09:32:54 +00:00
cmd := & cobra . Command {
2020-11-30 09:37:17 +00:00
Use : "dashboard [command]" ,
2021-04-16 08:50:08 +00:00
Short : "Manage your metabase dashboard container [requires local API]" ,
Long : ` Install / Start / Stop / Remove a metabase container exposing dashboard and metrics .
Note : This command requires database direct access , so is intended to be run on Local API / master .
` ,
2021-08-31 13:03:47 +00:00
Args : cobra . ExactArgs ( 1 ) ,
DisableAutoGenTag : true ,
2020-11-30 09:37:17 +00:00
Example : `
cscli dashboard setup
2020-05-15 09:39:16 +00:00
cscli dashboard start
cscli dashboard stop
2020-11-30 09:37:17 +00:00
cscli dashboard remove
` ,
2023-07-27 15:02:20 +00:00
PersistentPreRunE : func ( cmd * cobra . Command , args [ ] string ) error {
if err := require . LAPI ( csConfig ) ; err != nil {
return err
2021-07-01 11:41:16 +00:00
}
2023-07-27 15:02:20 +00:00
if err := metabase . TestAvailability ( ) ; err != nil {
return err
2021-04-16 08:50:08 +00:00
}
2020-11-30 09:37:17 +00:00
metabaseConfigFolderPath := filepath . Join ( csConfig . ConfigPaths . ConfigDir , metabaseConfigFolder )
metabaseConfigPath = filepath . Join ( metabaseConfigFolderPath , metabaseConfigFile )
if err := os . MkdirAll ( metabaseConfigFolderPath , os . ModePerm ) ; err != nil {
2023-07-27 15:02:20 +00:00
return err
2020-11-30 09:37:17 +00:00
}
2023-07-27 15:02:20 +00:00
if err := require . DB ( csConfig ) ; err != nil {
return err
2021-03-31 09:32:38 +00:00
}
2021-07-01 16:15:22 +00:00
/ *
Old container name was "/crowdsec-metabase" but podman doesn ' t
allow '/' in container name . We do this check to not break
existing dashboard setup .
* /
if ! metabase . IsContainerExist ( metabaseContainerID ) {
oldContainerID := fmt . Sprintf ( "/%s" , metabaseContainerID )
if metabase . IsContainerExist ( oldContainerID ) {
metabaseContainerID = oldContainerID
}
}
2023-07-27 15:02:20 +00:00
return nil
2020-11-30 09:37:17 +00:00
} ,
2020-05-15 09:39:16 +00:00
}
2023-12-11 09:32:54 +00:00
cmd . AddCommand ( cli . NewSetupCmd ( ) )
cmd . AddCommand ( cli . NewStartCmd ( ) )
cmd . AddCommand ( cli . NewStopCmd ( ) )
cmd . AddCommand ( cli . NewShowPasswordCmd ( ) )
cmd . AddCommand ( cli . NewRemoveCmd ( ) )
2023-01-06 22:14:02 +00:00
2023-12-11 09:32:54 +00:00
return cmd
2023-01-06 22:14:02 +00:00
}
2023-12-11 09:32:54 +00:00
func ( cli cliDashboard ) NewSetupCmd ( ) * cobra . Command {
2020-05-15 09:39:16 +00:00
var force bool
2023-01-06 22:14:02 +00:00
2023-12-11 09:32:54 +00:00
cmd := & cobra . Command {
2021-08-31 13:03:47 +00:00
Use : "setup" ,
Short : "Setup a metabase container." ,
Long : ` Perform a metabase docker setup, download standard dashboards, create a fresh user and start the container ` ,
Args : cobra . ExactArgs ( 0 ) ,
DisableAutoGenTag : true ,
2020-11-30 09:37:17 +00:00
Example : `
cscli dashboard setup
cscli dashboard setup -- listen 0.0 .0 .0
cscli dashboard setup - l 0.0 .0 .0 - p 443 -- password < password >
2020-05-15 09:39:16 +00:00
` ,
2023-07-25 13:21:25 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2020-11-30 09:37:17 +00:00
if metabaseDbPath == "" {
metabaseDbPath = csConfig . ConfigPaths . DataDir
2020-05-15 09:39:16 +00:00
}
2020-11-30 09:37:17 +00:00
if metabasePassword == "" {
2021-05-17 09:43:53 +00:00
isValid := passwordIsValid ( metabasePassword )
for ! isValid {
metabasePassword = generatePassword ( 16 )
isValid = passwordIsValid ( metabasePassword )
}
2020-05-15 09:39:16 +00:00
}
2023-07-25 13:21:25 +00:00
if err := checkSystemMemory ( & forceYes ) ; err != nil {
return err
2021-02-10 08:23:33 +00:00
}
2023-07-25 13:21:25 +00:00
warnIfNotLoopback ( metabaseListenAddress )
if err := disclaimer ( & forceYes ) ; err != nil {
return err
2021-02-10 08:23:33 +00:00
}
2023-07-25 13:21:25 +00:00
dockerGroup , err := checkGroups ( & forceYes )
2021-02-10 08:23:33 +00:00
if err != nil {
2023-07-25 13:21:25 +00:00
return err
2021-02-10 08:23:33 +00:00
}
2023-12-04 10:06:41 +00:00
if err = chownDatabase ( dockerGroup . Gid ) ; err != nil {
return err
}
2023-07-25 13:21:25 +00:00
mb , err := metabase . SetupMetabase ( csConfig . API . Server . DbConfig , metabaseListenAddress , metabaseListenPort , metabaseUser , metabasePassword , metabaseDbPath , dockerGroup . Gid , metabaseContainerID , metabaseImage )
2020-11-30 09:37:17 +00:00
if err != nil {
2023-07-25 13:21:25 +00:00
return err
2020-05-15 09:39:16 +00:00
}
2020-11-30 09:37:17 +00:00
if err := mb . DumpConfig ( metabaseConfigPath ) ; err != nil {
2023-07-25 13:21:25 +00:00
return err
2020-11-30 09:37:17 +00:00
}
log . Infof ( "Metabase is ready" )
fmt . Println ( )
fmt . Printf ( "\tURL : '%s'\n" , mb . Config . ListenURL )
fmt . Printf ( "\tusername : '%s'\n" , mb . Config . Username )
fmt . Printf ( "\tpassword : '%s'\n" , mb . Config . Password )
2023-07-25 13:21:25 +00:00
return nil
2020-05-15 09:39:16 +00:00
} ,
}
2023-12-11 09:32:54 +00:00
cmd . Flags ( ) . BoolVarP ( & force , "force" , "f" , false , "Force setup : override existing files" )
cmd . Flags ( ) . StringVarP ( & metabaseDbPath , "dir" , "d" , "" , "Shared directory with metabase container" )
cmd . Flags ( ) . StringVarP ( & metabaseListenAddress , "listen" , "l" , metabaseListenAddress , "Listen address of container" )
cmd . Flags ( ) . StringVar ( & metabaseImage , "metabase-image" , metabaseImage , "Metabase image to use" )
cmd . Flags ( ) . StringVarP ( & metabaseListenPort , "port" , "p" , metabaseListenPort , "Listen port of container" )
cmd . Flags ( ) . BoolVarP ( & forceYes , "yes" , "y" , false , "force yes" )
//cmd.Flags().StringVarP(&metabaseUser, "user", "u", "crowdsec@crowdsec.net", "metabase user")
cmd . Flags ( ) . StringVar ( & metabasePassword , "password" , "" , "metabase password" )
return cmd
2023-01-06 22:14:02 +00:00
}
2020-05-15 09:39:16 +00:00
2023-12-11 09:32:54 +00:00
func ( cli cliDashboard ) NewStartCmd ( ) * cobra . Command {
cmd := & cobra . Command {
2021-08-31 13:03:47 +00:00
Use : "start" ,
Short : "Start the metabase container." ,
Long : ` Stats the metabase container using docker. ` ,
Args : cobra . ExactArgs ( 0 ) ,
DisableAutoGenTag : true ,
2023-07-25 13:21:25 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2021-07-01 16:15:22 +00:00
mb , err := metabase . NewMetabase ( metabaseConfigPath , metabaseContainerID )
2020-11-30 09:37:17 +00:00
if err != nil {
2023-07-25 13:21:25 +00:00
return err
}
warnIfNotLoopback ( mb . Config . ListenAddr )
if err := disclaimer ( & forceYes ) ; err != nil {
return err
2020-11-30 09:37:17 +00:00
}
if err := mb . Container . Start ( ) ; err != nil {
2023-07-25 13:21:25 +00:00
return fmt . Errorf ( "failed to start metabase container : %s" , err )
2020-05-15 09:39:16 +00:00
}
log . Infof ( "Started metabase" )
2023-07-25 13:21:25 +00:00
log . Infof ( "url : http://%s:%s" , mb . Config . ListenAddr , mb . Config . ListenPort )
return nil
2020-05-15 09:39:16 +00:00
} ,
}
2023-12-11 09:32:54 +00:00
cmd . Flags ( ) . BoolVarP ( & forceYes , "yes" , "y" , false , "force yes" )
2024-01-03 09:55:41 +00:00
2023-12-11 09:32:54 +00:00
return cmd
2023-01-06 22:14:02 +00:00
}
2020-05-15 09:39:16 +00:00
2023-12-11 09:32:54 +00:00
func ( cli cliDashboard ) NewStopCmd ( ) * cobra . Command {
cmd := & cobra . Command {
2021-08-31 13:03:47 +00:00
Use : "stop" ,
Short : "Stops the metabase container." ,
Long : ` Stops the metabase container using docker. ` ,
Args : cobra . ExactArgs ( 0 ) ,
DisableAutoGenTag : true ,
2023-07-25 13:21:25 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2020-11-30 09:37:17 +00:00
if err := metabase . StopContainer ( metabaseContainerID ) ; err != nil {
2023-07-25 13:21:25 +00:00
return fmt . Errorf ( "unable to stop container '%s': %s" , metabaseContainerID , err )
2020-05-15 09:39:16 +00:00
}
2023-07-25 13:21:25 +00:00
return nil
2020-05-15 09:39:16 +00:00
} ,
}
2024-01-03 09:55:41 +00:00
2023-12-11 09:32:54 +00:00
return cmd
2023-01-06 22:14:02 +00:00
}
2023-12-11 09:32:54 +00:00
func ( cli cliDashboard ) NewShowPasswordCmd ( ) * cobra . Command {
cmd := & cobra . Command { Use : "show-password" ,
2022-03-10 12:23:21 +00:00
Short : "displays password of metabase." ,
Args : cobra . ExactArgs ( 0 ) ,
DisableAutoGenTag : true ,
2023-07-25 13:21:25 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2022-03-10 12:23:21 +00:00
m := metabase . Metabase { }
if err := m . LoadConfig ( metabaseConfigPath ) ; err != nil {
2023-07-25 13:21:25 +00:00
return err
2022-03-10 12:23:21 +00:00
}
2022-05-17 14:57:04 +00:00
log . Printf ( "'%s'" , m . Config . Password )
2023-07-25 13:21:25 +00:00
return nil
2022-03-10 12:23:21 +00:00
} ,
}
2024-01-03 09:55:41 +00:00
2023-12-11 09:32:54 +00:00
return cmd
2023-01-06 22:14:02 +00:00
}
2023-12-11 09:32:54 +00:00
func ( cli cliDashboard ) NewRemoveCmd ( ) * cobra . Command {
2023-01-06 22:14:02 +00:00
var force bool
2022-03-10 12:23:21 +00:00
2023-12-11 09:32:54 +00:00
cmd := & cobra . Command {
2021-08-31 13:03:47 +00:00
Use : "remove" ,
Short : "removes the metabase container." ,
Long : ` removes the metabase container using docker. ` ,
Args : cobra . ExactArgs ( 0 ) ,
DisableAutoGenTag : true ,
2020-11-30 09:37:17 +00:00
Example : `
cscli dashboard remove
cscli dashboard remove -- force
` ,
2023-07-25 13:21:25 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2020-11-30 09:37:17 +00:00
if ! forceYes {
2023-07-25 13:21:25 +00:00
var answer bool
2020-11-30 09:37:17 +00:00
prompt := & survey . Confirm {
Message : "Do you really want to remove crowdsec dashboard? (all your changes will be lost)" ,
Default : true ,
}
if err := survey . AskOne ( prompt , & answer ) ; err != nil {
2023-07-25 13:21:25 +00:00
return fmt . Errorf ( "unable to ask to force: %s" , err )
}
if ! answer {
return fmt . Errorf ( "user stated no to continue" )
2020-11-30 09:37:17 +00:00
}
}
2023-07-25 13:21:25 +00:00
if metabase . IsContainerExist ( metabaseContainerID ) {
log . Debugf ( "Stopping container %s" , metabaseContainerID )
if err := metabase . StopContainer ( metabaseContainerID ) ; err != nil {
log . Warningf ( "unable to stop container '%s': %s" , metabaseContainerID , err )
}
dockerGroup , err := user . LookupGroup ( crowdsecGroup )
if err == nil { // if group exist, remove it
groupDelCmd , err := exec . LookPath ( "groupdel" )
if err != nil {
return fmt . Errorf ( "unable to find 'groupdel' command, can't continue" )
2021-02-10 08:23:33 +00:00
}
2023-07-25 13:21:25 +00:00
groupDel := & exec . Cmd { Path : groupDelCmd , Args : [ ] string { groupDelCmd , crowdsecGroup } }
if err := groupDel . Run ( ) ; err != nil {
log . Warnf ( "unable to delete group '%s': %s" , dockerGroup , err )
2020-11-30 09:37:17 +00:00
}
}
2023-07-25 13:21:25 +00:00
log . Debugf ( "Removing container %s" , metabaseContainerID )
if err := metabase . RemoveContainer ( metabaseContainerID ) ; err != nil {
log . Warnf ( "unable to remove container '%s': %s" , metabaseContainerID , err )
}
log . Infof ( "container %s stopped & removed" , metabaseContainerID )
}
log . Debugf ( "Removing metabase db %s" , csConfig . ConfigPaths . DataDir )
if err := metabase . RemoveDatabase ( csConfig . ConfigPaths . DataDir ) ; err != nil {
log . Warnf ( "failed to remove metabase internal db : %s" , err )
}
if force {
m := metabase . Metabase { }
if err := m . LoadConfig ( metabaseConfigPath ) ; err != nil {
return err
2020-11-30 09:37:17 +00:00
}
2023-07-25 13:21:25 +00:00
if err := metabase . RemoveImageContainer ( m . Config . Image ) ; err != nil {
if ! strings . Contains ( err . Error ( ) , "No such image" ) {
return fmt . Errorf ( "removing docker image: %s" , err )
2020-11-30 09:37:17 +00:00
}
}
}
2023-07-25 13:21:25 +00:00
return nil
2020-05-15 09:39:16 +00:00
} ,
}
2023-12-11 09:32:54 +00:00
cmd . Flags ( ) . BoolVarP ( & force , "force" , "f" , false , "Remove also the metabase image" )
cmd . Flags ( ) . BoolVarP ( & forceYes , "yes" , "y" , false , "force yes" )
2020-05-15 09:39:16 +00:00
2023-12-11 09:32:54 +00:00
return cmd
2020-05-15 09:39:16 +00:00
}
2021-05-17 09:43:53 +00:00
func passwordIsValid ( password string ) bool {
hasDigit := false
2023-12-11 09:32:54 +00:00
2021-05-17 09:43:53 +00:00
for _ , j := range password {
if unicode . IsDigit ( j ) {
hasDigit = true
2023-12-11 09:32:54 +00:00
2021-05-17 09:43:53 +00:00
break
}
}
if ! hasDigit || len ( password ) < 6 {
return false
}
2024-01-03 09:55:41 +00:00
2021-05-17 09:43:53 +00:00
return true
}
2022-05-18 09:05:01 +00:00
2023-07-25 13:21:25 +00:00
func checkSystemMemory ( forceYes * bool ) error {
2022-05-18 09:05:01 +00:00
totMem := memory . TotalMemory ( )
2023-07-25 13:21:25 +00:00
if totMem >= uint64 ( math . Pow ( 2 , 30 ) ) {
return nil
}
2024-01-03 09:55:41 +00:00
2023-07-25 13:21:25 +00:00
if ! * forceYes {
var answer bool
2024-01-03 09:55:41 +00:00
2023-07-25 13:21:25 +00:00
prompt := & survey . Confirm {
Message : "Metabase requires 1-2GB of RAM, your system is below this requirement continue ?" ,
Default : true ,
}
if err := survey . AskOne ( prompt , & answer ) ; err != nil {
return fmt . Errorf ( "unable to ask about RAM check: %s" , err )
}
2024-01-03 09:55:41 +00:00
2023-07-25 13:21:25 +00:00
if ! answer {
return fmt . Errorf ( "user stated no to continue" )
}
2024-01-03 09:55:41 +00:00
2023-07-25 13:21:25 +00:00
return nil
}
2024-01-03 09:55:41 +00:00
2023-07-25 13:21:25 +00:00
log . Warn ( "Metabase requires 1-2GB of RAM, your system is below this requirement" )
2024-01-03 09:55:41 +00:00
2023-07-25 13:21:25 +00:00
return nil
}
func warnIfNotLoopback ( addr string ) {
if addr == "127.0.0.1" || addr == "::1" {
return
}
2024-01-03 09:55:41 +00:00
2023-07-25 13:21:25 +00:00
log . Warnf ( "You are potentially exposing your metabase port to the internet (addr: %s), please consider using a reverse proxy" , addr )
}
func disclaimer ( forceYes * bool ) error {
if ! * forceYes {
var answer bool
2024-01-03 09:55:41 +00:00
2023-07-25 13:21:25 +00:00
prompt := & survey . Confirm {
Message : "CrowdSec takes no responsibility for the security of your metabase instance. Do you accept these responsibilities ?" ,
Default : true ,
}
2024-01-03 09:55:41 +00:00
2023-07-25 13:21:25 +00:00
if err := survey . AskOne ( prompt , & answer ) ; err != nil {
return fmt . Errorf ( "unable to ask to question: %s" , err )
}
2024-01-03 09:55:41 +00:00
2023-07-25 13:21:25 +00:00
if ! answer {
return fmt . Errorf ( "user stated no to responsibilities" )
}
2024-01-03 09:55:41 +00:00
2023-07-25 13:21:25 +00:00
return nil
}
2024-01-03 09:55:41 +00:00
2023-07-25 13:21:25 +00:00
log . Warn ( "CrowdSec takes no responsibility for the security of your metabase instance. You used force yes, so you accept this disclaimer" )
2024-01-03 09:55:41 +00:00
2023-07-25 13:21:25 +00:00
return nil
}
func checkGroups ( forceYes * bool ) ( * user . Group , error ) {
dockerGroup , err := user . LookupGroup ( crowdsecGroup )
if err == nil {
2023-12-04 10:06:41 +00:00
return dockerGroup , nil
2023-07-25 13:21:25 +00:00
}
2024-01-03 09:55:41 +00:00
2023-12-04 10:06:41 +00:00
if ! * forceYes {
var answer bool
2024-01-03 09:55:41 +00:00
2023-12-04 10:06:41 +00:00
prompt := & survey . Confirm {
Message : fmt . Sprintf ( "For metabase docker to be able to access SQLite file we need to add a new group called '%s' to the system, is it ok for you ?" , crowdsecGroup ) ,
Default : true ,
2023-07-25 13:21:25 +00:00
}
2024-01-03 09:55:41 +00:00
2023-12-04 10:06:41 +00:00
if err := survey . AskOne ( prompt , & answer ) ; err != nil {
return dockerGroup , fmt . Errorf ( "unable to ask to question: %s" , err )
2023-07-25 13:21:25 +00:00
}
2024-01-03 09:55:41 +00:00
2023-12-04 10:06:41 +00:00
if ! answer {
return dockerGroup , fmt . Errorf ( "unable to continue without creating '%s' group" , crowdsecGroup )
2023-07-25 13:21:25 +00:00
}
}
2024-01-03 09:55:41 +00:00
2023-12-04 10:06:41 +00:00
groupAddCmd , err := exec . LookPath ( "groupadd" )
2023-07-25 13:21:25 +00:00
if err != nil {
2023-12-04 10:06:41 +00:00
return dockerGroup , fmt . Errorf ( "unable to find 'groupadd' command, can't continue" )
2022-05-18 09:05:01 +00:00
}
2023-12-04 10:06:41 +00:00
groupAdd := & exec . Cmd { Path : groupAddCmd , Args : [ ] string { groupAddCmd , crowdsecGroup } }
if err := groupAdd . Run ( ) ; err != nil {
return dockerGroup , fmt . Errorf ( "unable to add group '%s': %s" , dockerGroup , err )
2022-05-18 09:05:01 +00:00
}
2024-01-03 09:55:41 +00:00
2023-12-04 10:06:41 +00:00
return user . LookupGroup ( crowdsecGroup )
}
func chownDatabase ( gid string ) error {
intID , err := strconv . Atoi ( gid )
if err != nil {
return fmt . Errorf ( "unable to convert group ID to int: %s" , err )
}
2024-01-03 09:55:41 +00:00
2023-12-04 10:06:41 +00:00
if stat , err := os . Stat ( csConfig . DbConfig . DbPath ) ; ! os . IsNotExist ( err ) {
info := stat . Sys ( )
if err := os . Chown ( csConfig . DbConfig . DbPath , int ( info . ( * syscall . Stat_t ) . Uid ) , intID ) ; err != nil {
return fmt . Errorf ( "unable to chown sqlite db file '%s': %s" , csConfig . DbConfig . DbPath , err )
}
}
2024-01-03 09:55:41 +00:00
2023-12-04 10:06:41 +00:00
if csConfig . DbConfig . Type == "sqlite" && csConfig . DbConfig . UseWal != nil && * csConfig . DbConfig . UseWal {
for _ , ext := range [ ] string { "-wal" , "-shm" } {
file := csConfig . DbConfig . DbPath + ext
if stat , err := os . Stat ( file ) ; ! os . IsNotExist ( err ) {
info := stat . Sys ( )
if err := os . Chown ( file , int ( info . ( * syscall . Stat_t ) . Uid ) , intID ) ; err != nil {
return fmt . Errorf ( "unable to chown sqlite db file '%s': %s" , file , err )
}
}
}
}
2024-01-03 09:55:41 +00:00
2023-12-04 10:06:41 +00:00
return nil
2022-05-18 09:05:01 +00:00
}