diff --git a/.github/workflows/anchore-syft.yml b/.forgejo/workflows/anchore-syft.yml similarity index 100% rename from .github/workflows/anchore-syft.yml rename to .forgejo/workflows/anchore-syft.yml diff --git a/.github/workflows/codeql.yml b/.forgejo/workflows/codeql.yml similarity index 100% rename from .github/workflows/codeql.yml rename to .forgejo/workflows/codeql.yml diff --git a/.forgejo/workflows/debian.yml b/.forgejo/workflows/debian.yml new file mode 100644 index 0000000..0a0ed7e --- /dev/null +++ b/.forgejo/workflows/debian.yml @@ -0,0 +1,78 @@ +name: Create the timetracker DEB + +permissions: + contents: write + +on: + push: + tags: + - 'v*' + +jobs: + Create_Packages: + name: Create Package + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: https://code.forgejo.org/actions/setup-go@v5 + with: + go-version: '1.18' + + - name: Tidy dependencies + run: go mod tidy + + - name: Build + run: go build -o timetracker cmd/main.go + + - name: Copy necessary files + run: | + mkdir -p PKG_SOURCE/usr/local/bin + mkdir -p PKG_SOURCE/var/lib/timetracker + mkdir -p PKG_SOURCE/lib/systemd/system + cp -Rf ./DEBIAN PKG_SOURCE/ + cp -Rf ./timetracker PKG_SOURCE/var/lib/timetracker/ + cp -Rf ./scripts/* PKG_SOURCE/var/lib/timetracker/ + cp -Rf ./systemd/* PKG_SOURCE/lib/systemd/system/ + + - name: Create Deb package + run: | + dpkg-deb --build PKG_SOURCE timetracker_${{github.ref_name}}.deb + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.test }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.test }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: timetracker_${{github.ref_name}}.deb + asset_name: timetracker_${{github.ref_name}}.deb + asset_content_type: application/zip + - name: Upload Artifact (für CI-Artefakt) + uses: actions/upload-artifact@v3 + with: + name: timetracker_${{github.ref_name}}.deb + path: timetracker_${{github.ref_name}}.deb + retention-days: 5 + token: ${{ secrets.test }} + - name: Upload Package + run: | + FILE="timetracker_${{github.ref_name}}.deb" + ORG="Ronny.Friedland" + REPO="timetracker" + VERSION=${{github.ref_name}} + curl -v --header "Authorization: Bearer ${{ secrets.TEST }}" \ + --upload-file $FILE \ + "https://edp.buildth.ing/api/packages/${ORG}/generic/${REPO}/${VERSION}/${FILE}" diff --git a/.github/workflows/go.yml b/.forgejo/workflows/go.yml similarity index 100% rename from .github/workflows/go.yml rename to .forgejo/workflows/go.yml diff --git a/.forgejo/workflows/openbao.yml b/.forgejo/workflows/openbao.yml new file mode 100644 index 0000000..a74d4e4 --- /dev/null +++ b/.forgejo/workflows/openbao.yml @@ -0,0 +1,23 @@ +name: openbao + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - name: Read Openbao secrets + id: read-openbao-secrets + uses: hashicorp/vault-action@v2 + with: + url: https://vault-test.mms-at-work.de:8200 + token: ${{ secrets.VAULT_TEST_TOKEN }} + secrets: | + testproject/test/testproject foo | FOO + - name: Echo secret value from Openbao + run: echo "$FOO" \ No newline at end of file diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml deleted file mode 100644 index 704f950..0000000 --- a/.github/workflows/debian.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Create the timetracker DEB - -permissions: - contents: write - -on: - push: - tags: - - 'v*' - -jobs: - Create_Packages: - name: Create Package - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Build - run: go build -o timetracker cmd/main.go - - - name: Copy necessary files - run: | - mkdir -p PKG_SOURCE/usr/local/bin - mkdir -p PKG_SOURCE/var/lib/timetracker - mkdir -p PKG_SOURCE/lib/systemd/system - cp -Rf ./DEBIAN PKG_SOURCE/ - cp -Rf ./timetracker PKG_SOURCE/usr/local/bin/ - cp -Rf ./systemd/* PKG_SOURCE/lib/systemd/system/ - - - name: Create Deb package - run: | - dpkg-deb --build PKG_SOURCE timetracker_${{github.ref_name}}.deb - - - name: Release the Deb package - uses: softprops/action-gh-release@v1 - with: - files: timetracker_${{github.ref_name}}.deb \ No newline at end of file diff --git a/DEBIAN/control b/DEBIAN/control index 37be5dd..dfbc182 100644 --- a/DEBIAN/control +++ b/DEBIAN/control @@ -1,5 +1,5 @@ Package: timetracker -Version: 1.0.2 +Version: 1.2.0 Section: misc Priority: optional Architecture: all diff --git a/DEBIAN/postinst b/DEBIAN/postinst index 3798664..4f83dd5 100755 --- a/DEBIAN/postinst +++ b/DEBIAN/postinst @@ -5,6 +5,11 @@ set -e case "$1" in configure) chown -R timetracker:timetracker /var/lib/timetracker + + chmod u+x /var/lib/timetracker/timetracker*.sh + + update-alternatives --install /usr/local/bin/timetracker timetracker /var/lib/timetracker/timetracker-archive.sh 100 + update-alternatives --install /usr/local/bin/timetracker timetracker /var/lib/timetracker/timetracker.sh 1000 ;; abort-upgrade|abort-remove|abort-deconfigure) diff --git a/DEBIAN/prerm b/DEBIAN/prerm index f536fcb..09c5cad 100755 --- a/DEBIAN/prerm +++ b/DEBIAN/prerm @@ -10,6 +10,8 @@ case "$1" in if getent group timetracker >/dev/null; then delgroup --system timetracker fi + + update-alternatives --remove-all timetracker ;; upgrade) diff --git a/Readme.md b/Readme.md index 25859e7..5fc9900 100644 --- a/Readme.md +++ b/Readme.md @@ -11,12 +11,21 @@ The timetracker application provides the following arguments which can be passed | Property | Description | |---------------|----------------------------------------------------------------------------| +| archivedata | Enables archiving timetracker status to excel archive file, default: false | | configpath | Defines the location of the necessary files, default: /var/lib/timetracker | ## Execution The application is triggered by a systemd timer which triggers the application via systemd unit. +*Note:* Running timetracker with systemd unit uses the default property values. To change it you have use the appropriate alternative. + +### Switch alternative + +```shell +update-alternatives --config timetracker +``` + To enable the timer you have to (requires root privileges): ### enable the timer diff --git a/cmd/main.go b/cmd/main.go index 9bb302a..41097f1 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -7,27 +7,28 @@ import ( ) func main() { - mode, configPath := parseArguments() + mode, configPath, archiveData := parseArguments() if mode == "" { os.Exit(0) } else if mode == "cli" { - cli.Run(&configPath) + cli.Run(&configPath, &archiveData) os.Exit(0) } else { os.Exit(1) } } -func parseArguments() (string, string) { +func parseArguments() (string, string, bool) { mode := flag.String("mode", "cli", "the application mode, available: cli") configPath := flag.String("configpath", "/var/lib/timetracker", "the config path") + archiveData := flag.Bool("archivedata", false, "flag to enable data archiving") help := flag.Bool("help", false, "print this help message") flag.Parse() if *help { flag.PrintDefaults() - return "", "" + return "", "", false } else { - return *mode, *configPath + return *mode, *configPath, *archiveData } } diff --git a/cmd/main_test.go b/cmd/main_test.go index b909b74..3af1815 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -11,18 +11,21 @@ func TestParameter(t *testing.T) { oldArgs := os.Args defer func() { os.Args = oldArgs }() - params := []string{"-configpath", "/foo", "-mode", "testmode"} + params := []string{"-mode", "testmode", "-configpath", "/foo", "-archivedata", "true"} flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) os.Args = append([]string{"params"}, params...) - mode, configPath := parseArguments() + mode, configPath, archiveData := parseArguments() if mode != "testmode" { log.Fatalf("Got unexpected mode result, got %s, expected %s", "testmode", mode) } if configPath != "/foo" { log.Fatalf("Got unexpected config path result, got %s, expected %s", "/foo", configPath) } + if !archiveData { + log.Fatalf("Got unexpected archive data flag result, got %t, expected %t", true, archiveData) + } } func TestParameterDefaults(t *testing.T) { @@ -34,13 +37,16 @@ func TestParameterDefaults(t *testing.T) { flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) os.Args = append([]string{"defaults"}, params...) - mode, configPath := parseArguments() + mode, configPath, archiveData := parseArguments() if mode != "cli" { log.Fatalf("Got unexpected mode result, got %s, expected %s", "cli", mode) } if configPath != "/var/lib/timetracker" { log.Fatalf("Got unexpected config path result, got %s, expected %s", "/var/lib/timetracker", configPath) } + if archiveData { + log.Fatalf("Got unexpected archive data flag result, got %t, expected %t", false, archiveData) + } } func TestParameterHelp(t *testing.T) { @@ -52,11 +58,14 @@ func TestParameterHelp(t *testing.T) { flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) os.Args = append([]string{"help"}, params...) - mode, configPath := parseArguments() + mode, configPath, archiveData := parseArguments() if mode != "" { log.Fatalf("Got unexpected mode result, got %s, expected %s", "", mode) } if configPath != "" { log.Fatalf("Got unexpected config path result, got %s, expected %s", "", configPath) } + if archiveData { + log.Fatalf("Got unexpected archive data flag result, got %t, expected %t", false, archiveData) + } } diff --git a/go.mod b/go.mod index 11b91b5..f179543 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,16 @@ module ronnyfriedland/timetracker/v2 go 1.18 + +require github.com/xuri/excelize/v2 v2.9.0 + +require ( + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 // indirect + github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/text v0.23.0 // indirect +) diff --git a/go.sum b/go.sum index e69de29..e6e8908 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,24 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 h1:78nKszZqigiBRBVcoe/AuPzyLTWW5B+ltBaUX1rlIXA= +github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= +github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= +github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba h1:DhIu6n3qU0joqG9f4IO6a/Gkerd+flXrmlJ+0yX2W8U= +github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 0229763..b33ea19 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -2,14 +2,17 @@ package cli import ( "log" + + "ronnyfriedland/timetracker/v2/internal/excel" "ronnyfriedland/timetracker/v2/internal/logic" ) const dateLayout = "02.01.2006" const timeLayout = "15:04:05" -func Run(configPath *string) { +func Run(configPath *string, archiveData *bool) { duration := logic.Execute(configPath) + if duration.Complete { log.Printf("[%s] - Work duration: %2.2fh", duration.Date.Format(dateLayout), @@ -20,5 +23,8 @@ func Run(configPath *string) { duration.StartTime.Format(timeLayout), duration.EndTime.Format(timeLayout)) + if *archiveData { + excel.Export(configPath, duration) + } } } diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 9067166..fe27786 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -16,8 +16,9 @@ func TestRunNotComplete(t *testing.T) { log.SetOutput(&logContent) directory, _ := createStatusFile() + archiveData := false - Run(&directory) + Run(&directory, &archiveData) if logContent.String() != "" { log.Fatalf("Expected empty logmessage") @@ -31,10 +32,11 @@ func TestRunComplete(t *testing.T) { log.SetOutput(&logContent) directory, fileName := createStatusFile() + archiveData := false setModificationDate(fileName, "28.02.2022") - Run(&directory) + Run(&directory, &archiveData) if logContent.String() == "" { log.Fatalf("Expected logmessage") diff --git a/internal/excel/excel.go b/internal/excel/excel.go new file mode 100644 index 0000000..7df466c --- /dev/null +++ b/internal/excel/excel.go @@ -0,0 +1,61 @@ +package excel + +import ( + "fmt" + "log" + "time" + + "github.com/xuri/excelize/v2" + + "ronnyfriedland/timetracker/v2/internal/logic" +) + +var SheetName = time.Now().Format("2006") +var Headers = []string{"Date", "From", "To", "Duration"} + +const dateLayout = "02.01.2006" +const timeLayout = "15:04:05" + +func Export(configPath *string, duration logic.Duration) string { + archiveDataFile := *configPath + "/timetracker.xlsx" + + file, err := excelize.OpenFile(archiveDataFile) + if err != nil { + file = excelize.NewFile() + } + defer func() { + err := file.Close() + if err != nil { + log.Fatal(err) + } + }() + + _, err = file.NewSheet(SheetName) + if err != nil { + log.Fatalf("Failed to ensure sheet created: %v", err) + } + file.DeleteSheet("Sheet1") + + for i, header := range Headers { + file.SetCellValue(SheetName, fmt.Sprintf("%s%d", string(rune(65+i)), 1), header) + } + + rows, err := file.GetRows(SheetName) + if err != nil { + log.Fatalf("Failed to get rows from sheet: %v", err) + } + + next := len(rows) + 1 + + file.SetCellValue(SheetName, fmt.Sprintf("A%d", next), duration.Date.Format(dateLayout)) + file.SetCellValue(SheetName, fmt.Sprintf("B%d", next), duration.StartTime.Format(timeLayout)) + file.SetCellValue(SheetName, fmt.Sprintf("C%d", next), duration.EndTime.Format(timeLayout)) + file.SetCellValue(SheetName, fmt.Sprintf("D%d", next), fmt.Sprintf("%2.2f", duration.Duration.Hours())) + + if err := file.SaveAs(archiveDataFile); + err != nil { + log.Fatal(err) + } + + return archiveDataFile +} diff --git a/internal/excel/excel_test.go b/internal/excel/excel_test.go new file mode 100644 index 0000000..8eef735 --- /dev/null +++ b/internal/excel/excel_test.go @@ -0,0 +1,47 @@ +package excel + +import ( + "log" + "os" + "ronnyfriedland/timetracker/v2/internal/logic" + "testing" + "time" + + "github.com/xuri/excelize/v2" +) + +func TestExport(t *testing.T) { + + testData := logic.Duration{ + Date: time.Now(), + StartTime: time.Now(), + EndTime: time.Now(), + Duration: time.Duration(1000), + } + + excelDirectory := os.TempDir() + excelFile := Export(&excelDirectory, testData) + + file, err := excelize.OpenFile(excelFile) + if err != nil { + log.Fatalf("Expected excel file") + } + + rows, err := file.GetRows(SheetName) + if err != nil { + log.Fatalf("Expected no error getting rows") + } + + if len(rows) != 2 { + log.Fatalf("Unexpected line count - expected 2, got %d", len(rows)) + } + + headers := rows[0] + for i := 0; i < len(headers); i++ { + if headers[i] != Headers[i] { + log.Fatalf("Unexpected header value - expected %s, got %s", Headers[i], headers[i]) + } + } + + defer os.Remove(excelFile) +} diff --git a/scripts/timetracker-archive.sh b/scripts/timetracker-archive.sh new file mode 100644 index 0000000..0fd9b50 --- /dev/null +++ b/scripts/timetracker-archive.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +/var/lib/timetracker/timetracker -archivedata=true \ No newline at end of file diff --git a/scripts/timetracker.sh b/scripts/timetracker.sh new file mode 100644 index 0000000..b8e26f9 --- /dev/null +++ b/scripts/timetracker.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +/var/lib/timetracker/timetracker \ No newline at end of file