Compare commits

...

2 Commits

Author SHA1 Message Date
Drew Bednar 840b84b120 Adding Cobra CLI to project (#2)
continuous-integration/drone/push Build is passing Details
I would like to have a composable cli tool with subcommands for management of the Flux Feed application. This PR restructures the cmd package according to conventions outlined in the Cobra user guide https://github.com/spf13/cobra/blob/main/site/content/user_guide.md

At present only the database migrate command is implemented, and with minimal functionality. Future work will have to add additional flags and information on how to declare the location of the database to be migrated.

Reviewed-on: #2
6 months ago
Drew Bednar c3f271bcfd Adding SQL support to application. (#1)
continuous-integration/drone/push Build is passing Details
Adds new packages to support sqlite3 database as a supported backend.

Reviewed-on: #1
6 months ago

@ -0,0 +1,3 @@
FLUX_DATABASE_DRIVER=sqlite3
FLUX_DATABASE_DSN=./flux-local.db
FLUX_MIGRATIONS_PATH=./migrations

7
.gitignore vendored

@ -21,4 +21,9 @@
# Go workspace file
go.work
fluxfeed
# Flux Feed
fluxfeed
flux-feed
fluxctl
.env
flux-local.db

@ -15,11 +15,11 @@ vet: fmt
.PHONY:vet
build: vet
go build -o ./fluxfeed .
go build -o ./fluxctl .
.PHONY:build
clean:
rm -rf ./fluxfeed
rm -rf ./fluxctl
.PHONY:clean
docs:

@ -1,5 +1,25 @@
# Flux Feed
![build-status](https://drone.runcible.io/api/badges/androiddrew/flux-feed/status.svg)
[![](https://img.shields.io/badge/License-AGPL-blue.svg)](https://opensource.org/licenses/AGPL "License: AGPL")
An Indie Reader for the modern day.
An Indie Reader for the modern day.
## Building
From the root of the source tree run:
```shell
make build
```
## Using
```shell
./fluxctl --help
```
## License
is project is licensed under the MIT License. See the [LICENSE file](https://git.runcible.io/androiddrew/flux-feed/src/branch/main/LICENSE) for the full license text.

@ -0,0 +1,48 @@
// Package cmd migrate provides a database migration command to root cli
package cmd
import (
"database/sql"
"log"
"git.runcible.io/androiddrew/flux-feed/config"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite3"
_ "github.com/golang-migrate/migrate/v4/source/file" //migrations
_ "github.com/mattn/go-sqlite3" //migrations
"github.com/spf13/cobra"
)
func runMigrate(_ *cobra.Command, _ []string) {
cfg := config.New()
db, err := sql.Open(cfg.DatabaseDriver, cfg.DatabaseDSN)
defer db.Close()
err = db.Ping()
if err != nil {
log.Fatal(err)
}
log.Printf("Connected to Database: %s", cfg.DatabaseDSN)
driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
log.Printf("Using migrations path: %s", cfg.MigrationsPath)
m, err := migrate.NewWithDatabaseInstance(
cfg.MigrationsPath, cfg.DatabaseDriver, driver)
if err != nil {
log.Fatal(err)
}
log.Printf("Migrating: %s", cfg.DatabaseDSN)
err = m.Up()
if err != nil {
log.Fatal(err)
}
}
// MigrateCmd provides database migrations command for cli
var MigrateCmd = &cobra.Command{
Use: "migrate",
Short: "Run database migrations",
Run: runMigrate,
}

@ -0,0 +1,26 @@
// Package cmd provides cli commands
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "fluxctl",
Short: "A flexible indie reader for the modern day",
}
// Execute executes the root command.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
rootCmd.AddCommand(MigrateCmd)
}

@ -0,0 +1,26 @@
// Package config provides configuration structures and methods to load
// environment variables for the application.
package config
import (
"log"
"github.com/joho/godotenv"
)
// Config holds the configuration for the application, including the database settings. It contains multiple embedded configuration structs.
type Config struct {
Database
}
// New loads environment variables and returns a new configured Config instance.
func New() *Config {
err := godotenv.Load()
if err != nil {
log.Println(err)
}
return &Config{
Database: DataStore(),
}
}

@ -0,0 +1,18 @@
package config
import "github.com/kelseyhightower/envconfig"
// Database holds the configuration for the database connection.
type Database struct {
DatabaseDriver string `split_words:"true"`
DatabaseDSN string `split_words:"true"`
MigrationsPath string `default:"head" split_words:"true"`
}
// DataStore processes environment variables and returns a configured Database configuration struct.
func DataStore() Database {
var db Database
envconfig.MustProcess("flux", &db)
return db
}

@ -0,0 +1,33 @@
// Package database provides utility functions for connecting to and
// interacting with various database backends.
//
// This package supports multiple database drivers including:
// - sqlite3
// - others coming soon...maybe
package database
import (
"log"
"git.runcible.io/androiddrew/flux-feed/config"
"github.com/jmoiron/sqlx"
)
// NewSqlx configures a new sqlx.DB from application config
func NewSqlx(cfg config.Database) *sqlx.DB {
var dsn string
// TODO add additional database driver support
switch cfg.DatabaseDriver {
case "sqlite3":
dsn = cfg.DatabaseDSN
default:
log.Fatal("Must choose a database driver")
}
db := sqlx.MustConnect(cfg.DatabaseDriver, dsn)
return db
}

@ -1,3 +1,20 @@
module git.runcible.io/androiddrew/fluxfeed
module git.runcible.io/androiddrew/flux-feed
go 1.22.5
require (
github.com/golang-migrate/migrate/v4 v4.17.1
github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1
github.com/kelseyhightower/envconfig v1.4.0
github.com/mattn/go-sqlite3 v1.14.22
)
require (
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.uber.org/atomic v1.7.0 // indirect
)

@ -0,0 +1,43 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -1,17 +1,10 @@
// Flux Feed main entrypoint
package main
import (
"fmt"
"io"
"os"
"git.runcible.io/androiddrew/flux-feed/cmd"
)
const FluxGreeting string = "Welcome to Flux Feed\n"
func HelloFluxFeed(out io.Writer) {
fmt.Fprint(out, FluxGreeting)
}
func main() {
HelloFluxFeed(os.Stdout)
cmd.Execute()
}

@ -1,18 +0,0 @@
package main
import (
"bytes"
"testing"
)
func TestCliGreeting(t *testing.T) {
buffer := &bytes.Buffer{}
HelloFluxFeed(buffer)
got := buffer.String()
want := FluxGreeting
if got != want {
t.Errorf("Got %s but wanted %s", got, want)
}
}

@ -0,0 +1,3 @@
DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS feed;
DROP TABLE IF EXISTS entry;

@ -0,0 +1,72 @@
PRAGMA foreign_keys=1;
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
kindle_email TEXT
);
CREATE TABLE feed (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER UNIQUE NOT NULL,
url TEXT,
type TEXT NOT NULL,
name TEXT UNIQUE,
icon_url TEXT,
created TIMESTAMP NOT NULL,
updated TIMESTAMP NOT NULL,
last_fetch TIMESTAMP,
raw_data TEXT,
folder TEXT,
etag TEXT,
modified_header TEXT,
filters TEXT,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE entry (
id INTEGER PRIMARY KEY AUTOINCREMENT,
feed_id INTEGER UNIQUE,
user_id INTEGER NOT NULL,
remote_id TEXT UNIQUE NOT NULL,
title TEXT,
username TEXT,
user_url TEXT,
display_name TEXT,
avatar_url TEXT,
content_short TEXT,
content_full TEXT,
target_url TEXT,
content_url TEXT,
comments_url TEXT,
media_url TEXT,
created TIMESTAMP NOT NULL,
updated TIMESTAMP NOT NULL,
display_date TIMESTAMP NOT NULL,
sort_date TIMESTAMP NOT NULL,
viewed TIMESTAMP,
favorited TIMESTAMP,
pinned TIMESTAMP,
sent_to_kindle TIMESTAMP,
raw_data TEXT,
header TEXT,
icon_url TEXT,
FOREIGN KEY(feed_id) REFERENCES feed(id),
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE INDEX ix_feed_folder ON feed(folder);
CREATE INDEX ix_feed_user_id ON feed(user_id);
CREATE INDEX ix_name_user ON feed(user_id, name);
CREATE INDEX ix_feed_created ON feed(created);
CREATE INDEX ix_entry_favorited ON entry(favorited);
CREATE INDEX ix_entry_created ON entry(created);
CREATE INDEX ix_entry_sort_date ON entry(sort_date);
CREATE INDEX ix_entry_sent_to_kindle ON entry(sent_to_kindle);
CREATE INDEX entry_sort_ts ON entry(sort_date DESC);
CREATE INDEX ix_entry_viewed ON entry(viewed);
CREATE INDEX ix_entry_pinned ON entry(pinned);
CREATE INDEX ix_entry_user_id ON entry(user_id);
CREATE INDEX ix_entry_username ON entry(username);
Loading…
Cancel
Save