Compare commits

...

13 Commits
v0.0.2 ... main

3
.gitignore vendored

@ -19,3 +19,6 @@
# Go workspace file # Go workspace file
go.work go.work
/test
*.gif
*.webp

@ -4,3 +4,13 @@ pic - работа с картинками
## История ## История
- 29-07-2023 Первый коммит. - 29-07-2023 Первый коммит.
Сделал новый релиз для проверки - v0.0.2 Сделал новый релиз для проверки - v0.0.2
- 30-07-2023 Начинаю использовать ffmpeg.
- 04-08-2023 Сделал коммит на pc. Добавляю комментарии
## Комментарии
Сделал тесты для файлов.
Все-таки решил остановиться на формате url с http://.
Benchmark работает, но пока не понятно как интерпретировать результаты.

@ -35,7 +35,7 @@ func deleteImagesDB(db *sqlx.DB, ids []int) error {
return nil return nil
} }
// перемещает картинки в альбом // перемещает картинки ( список) в альбом
func moveImagesDB(db *sqlx.DB, newseria, ids string) error { func moveImagesDB(db *sqlx.DB, newseria, ids string) error {
cmd := "UPDATE pic SET seria = ? WHERE file_id = ?" cmd := "UPDATE pic SET seria = ? WHERE file_id = ?"
// fmt.Printf("перемещаю: %v в %s\n", ids, newseria) // fmt.Printf("перемещаю: %v в %s\n", ids, newseria)

@ -0,0 +1,56 @@
package pic
import (
"fmt"
"os"
"reflect"
"testing"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
// Это скорее интеграционное тестированиеЮ чем модульное
// Сначала создадим тестовую БД - test-sea аналог боевой
// Выполняется один раз при прогоне
func TextMain(m *testing.M) {
db = createTestDB()
os.Exit(m.Run())
}
func createTestDB() *sqlx.DB {
db, err := sqlx.Open("mysql", "itman:X753951x@(xigmanas:3306)")
if err != nil {
fmt.Printf("не удалось полключиться к БД: %v\n", err)
os.Exit(0)
}
return db
}
func Test_getAlbum(t *testing.T) {
type args struct {
db *sqlx.DB
name string
}
tests := []struct {
name string
args args
want []Picture
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getAlbum(tt.args.db, tt.args.name)
if (err != nil) != tt.wantErr {
t.Errorf("getAlbum() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("getAlbum() = %v, want %v", got, tt.want)
}
})
}
}

@ -1,8 +1,17 @@
module github.com/tad17/pic module test-pic
go 1.19 go 1.19
require ( require (
github.com/estebangarcia21/subprocess v0.0.0-20230526204252-a1a6de4773be
github.com/go-sql-driver/mysql v1.7.1 github.com/go-sql-driver/mysql v1.7.1
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
github.com/linxGnu/goseaweedfs v0.1.6
github.com/stretchr/testify v1.8.4
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

@ -1,3 +1,8 @@
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/estebangarcia21/subprocess v0.0.0-20230526204252-a1a6de4773be h1:Mf6Xc0DgENbsDgKn10KNCNmW9FYSTcMr2bFsJ9uzvX0=
github.com/estebangarcia21/subprocess v0.0.0-20230526204252-a1a6de4773be/go.mod h1:PlHe6+WP6t7m4ghYrX6GBzB0KZLdOKWz2Ih3h0nusAY=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
@ -5,5 +10,19 @@ github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/linxGnu/goseaweedfs v0.1.6 h1:qcCz0aBJlXVCVX+NiEnU+uGHMBvJR33U3PquzlEpcs8=
github.com/linxGnu/goseaweedfs v0.1.6/go.mod h1:cv43dFeG4S6HnLa7U9aYw+wD/uZEi66UmwKdforYuWo=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -2,31 +2,162 @@ package pic
import ( import (
"fmt" "fmt"
"github.com/jmoiron/sqlx" "io/ioutil"
"log"
"os"
"regexp"
"strings"
"github.com/estebangarcia21/subprocess"
//"github.com/jmoiron/sqlx"
) )
//Picture работа с картинкаи // Picture - работа с картинками
type Picture struct { type Picture struct {
ID int `json:"id"` ID int `db:"id" json:"id"`
Url string `json:"src"` Album string `db:"album" json:"album"`
Width int `json:"width"` Url string `db:"pic_url" json:"src"`
Height int `json:"height"` Width int `db:"pic_width" json:"width"`
Thumb string `json:"srct"` Height int `db:"pic_height" json:"height"`
Thumb string `db:"thumb_url" json:"srct"`
ThumbWidth int `db:"thumb_width" json:"imgtWidth"` ThumbWidth int `db:"thumb_width" json:"imgtWidth"`
ThumbHeight int `db:"thumb_height" json:"imgtHeight"` ThumbHeight int `db:"thumb_height" json:"imgtHeight"`
// закрытые поля
db *sqlx.DB
album string
} }
//Hello - приветствие и комментарии к модулю // Hello - приветствие и комментарии к модулю
func Hello() { func Hello() {
fmt.Printf("pic - пакет для работы с картинками\n") fmt.Printf("pic - пакет для работы с картинками\n")
fmt.Printf("v0.0.1 - начальная версия\n") fmt.Printf("v0.0.3 - промежуточная версия (не рабочая)\n")
} }
func NewPicture(db *sqlx.DB, album string) Picture { func NewPicture(album string) Picture {
pic := Picture {db: db, album: album} pic := Picture{Album: album}
return pic return pic
} }
// возвращает имя файла без расширения
// т.е. из test/filename.jpg получаем filename
func baseName(filename string) string {
// на всякий случай заменим обратный слэш
filename = strings.ReplaceAll(filename, "\\", "/")
// проверка на наличие каталогов
items := strings.Split(filename, "/")
if len(items) > 0 {
// есть каталоги, уберем
filename = items[len(items)-1]
}
fn := strings.Split(filename, ".")
return fn[0]
}
// cmdFfmpeg формирует команду ffmpeg для последующего исполнения
// ffmpeg - увеличивает gif в 2 раза и сразу преобразует в .webp
// filename - сам файл gif
// Возвращает команду ffmpeg и выходной файл webp
func cmdFfmpeg(src string) (string, string, error) {
if !strings.HasSuffix(src, ".gif") {
return "", "", fmt.Errorf("файл не .gif")
}
webp := strings.ReplaceAll(src, ".gif", "-2x.webp")
cmd := fmt.Sprintf("ffmpeg -i \"%s\" -vf scale=iw*2:ih*2 -loop 0 \"%s\"", src, webp)
return cmd, webp, nil
}
type WebpFile struct {
filename string
width string
height string
}
// конвертация из .gif в .webp с изменением размера файла
// с использованием программы ffmpeg.
// filename - исходный файл gif (полный путь)
// Возвращает описание выходного файла WebpFile
// Выходной файл находится рядом с исходным
func convertFile(filename string) (WebpFile, error) {
webpfile := WebpFile{}
cmd, webp, err := cmdFfmpeg(filename)
if err != nil {
return webpfile, err
}
s := subprocess.New(cmd, subprocess.HideStderr)
if err := s.Exec(); err != nil {
return webpfile, fmt.Errorf("(%s) ffmpeg: %v", filename, err)
}
if s.ExitCode() != 0 {
return webpfile, fmt.Errorf("(%s): ffmpeg exit code: %d", filename, s.ExitCode())
}
// из вывода команды ffmpeg получим новый размер файла webp
w, h := getSize(s.StderrText())
webpfile = WebpFile{
filename: webp,
width: w,
height: h,
}
// fid := upload(newname)
// println(fid)
return webpfile, nil
}
// Экспорт функции конвертации
func Convert(filename string) error {
webp, err := convertFile(filename)
if err != nil {
return err
}
log.Printf("[%sx%s] %s\n", webp.width, webp.height, webp.filename)
// все сделано, удалим исходный и выходной файл
if err := deleteFile(filename); err != nil {
return err
}
if err := deleteFile(webp.filename); err != nil {
return err
}
return nil
}
func deleteFile(filename string) error {
err := os.Remove(filename)
return err
}
// получает размер файла из вывода команды ffmpeg
// если пустая строка - бросаем панику
func getSize(out string) (string, string) {
if out == "" {
panic("пустая строка вывода ffmpeg")
}
lines := strings.Split(out, "\n")
for _, s := range lines {
if strings.Contains(s, "Stream #0:0: Video: webp") {
// fmt.Printf("== %s\n", s)
re := regexp.MustCompile(`(\d*)x(\d*)`)
matches := re.FindAllStringSubmatch(s, -1)
// fmt.Printf("matches: %v\n", matches)
return matches[0][1], matches[0][2]
}
}
return "0", "0"
}
func copyFile(src, dst string) error {
input, err := ioutil.ReadFile(src)
if err != nil {
return err
}
err = ioutil.WriteFile(dst, input, 0644)
if err != nil {
return err
}
return nil
}

@ -0,0 +1,92 @@
package pic
import (
//"log"
"log"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewPicture(t *testing.T) {
// для тестирования базу данных не учитываем
p := NewPicture("альбом")
assert.Equal(t, p.Album, "альбом", "должны быть одинаковыми")
}
func TestBaseName(t *testing.T) {
base := baseName("c:\\Проверка файла с каталогом\\test.jpg")
assert.Equal(t, base, "test", "должны быть одинаковыми")
base = baseName("/каталог файла/test.jpg")
assert.Equal(t, base, "test", "должны быть одинаковыми")
base = baseName("/test.jpg")
assert.Equal(t, base, "test", "должны быть одинаковыми")
base = baseName("test.jpg")
assert.Equal(t, base, "test", "должны быть одинаковыми")
base = baseName("test.jpg")
assert.NotEqual(t, base, "неверное имя", "должны быть разными")
}
func TestCmdFfmpeg(t *testing.T) {
// правильный файл
cmd, out, err := cmdFfmpeg("testdir\\test.gif")
log.Printf("cmd: %s\n", cmd)
assert.Nil(t, err)
assert.Equal(t, out, "testdir\\test-2x.webp", "должны быть одинаковыми")
assert.Contains(t, cmd, "ffmpeg", "команда должна сформироваться")
// не верный файл
cmd, out, err = cmdFfmpeg("test.jpg")
assert.Error(t, err, "должна быть ошибка")
assert.Equal(t, out, "", "должен быть пустой строкой")
assert.Equal(t, cmd, "", "должен быть пустой строкой")
}
func TestGetSize(t *testing.T) {
out := `
Stream #0:0: Video: webp <какой-то текст> 1200x1980 <еще какой-то текст>
`
w, h := getSize(out)
assert.Equal(t, w, "1200", "должны быть 1200")
assert.Equal(t, h, "1980", "должны быть 1980")
}
func TestConvertFile(t *testing.T) {
// предварительно скопируем тестовый файл во временный каталог
tmpgif := "test.gif"
require.FileExists(t, tmpgif)
// создадим временный каталог
tmpdir := t.TempDir()
filename := filepath.Join(tmpdir, tmpgif)
// скопируем исходный файл в этот каталог
err := copyFile(tmpgif, filename)
require.NoError(t, err)
require.FileExists(t, filename)
// собственно конвертация
webp, err := convertFile(filename)
assert.NoError(t, err)
require.FileExists(t, webp.filename)
// скопируем обратно, что бы посмотреть, что получилось
// err = copyFile(webp.filename, "d:/projects/pic/test.webp")
// assert.NoError(t, err)
}
func TestConvert(t *testing.T) {
// проверяем, что после конвертации файлы удаляются
tmpgif := "test.gif"
require.FileExists(t, tmpgif)
err := Convert(tmpgif)
assert.NoError(t, err)
}

@ -0,0 +1,44 @@
package pic
import (
"fmt"
"github.com/jmoiron/sqlx"
)
// SeaDB - картинки в БД
type SeaDB struct {
db *sqlx.DB
}
func NewSeaDB(db *sqlx.DB) (*SeaDB, error) {
if db == nil {
return nil, fmt.Errorf("db = nil")
}
seadb := SeaDB{db: db}
return &seadb, nil
}
// Возвращает список url-ов маленьких gif-ов из БД
func (sea *SeaDB) SmallGifs() ([]string, error) {
cmd := `
SELECT
newurl
FROM
filemeta
JOIN
pic ON filemeta.id = pic.file_id
WHERE
ext = '.gif'
and del = 0
and filemeta.converted = 0
and pic.width < 400
`
var urls []string
err := sea.db.Select(&urls, cmd)
if err != nil {
return nil, err
}
return urls, nil
}

@ -0,0 +1,90 @@
package pic
import (
"os"
"path/filepath"
"testing"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var db *sqlx.DB
// Для тестирования используем тестовую БД
func openDB() *sqlx.DB {
db, err := sqlx.Open("mysql", "itman:X753951x@(xigmanas:3306)/sea-test")
if err != nil {
panic(err)
}
return db
}
// Первоначальная установка окружения
func TestMain(m *testing.M) {
// Каталог test должен присутствовать и в нем должны находиться:
// sea-test.sql - скрипт для создания тестовой БД и заполнения ее тестовыми данными (mock)
// test.gif - тестовый gif (mock)
//os.RemoveAll("test")
//os.Mkdir("test", 0666)
db = openDB()
os.Exit(m.Run())
}
func TestGetSmallGifs(t *testing.T) {
// db открыта на глобальном уровне
seadb, err := NewSeaDB(db)
// если ошибка - дальнейшая проверка бессмыслена
require.NoError(t, err)
urls, err := seadb.SmallGifs()
assert.Nil(t, err)
// из тестовой БД должны получить 3 записи
assert.Equal(t, len(urls), 3, "должны получить ровно 3 записи")
}
// Проверка загрузки из weed
func TestDownload(t *testing.T) {
// используем временный каталог для тестирования
tmpdir := t.TempDir()
// для удобства - теперь вместо assert(t...) пишем просто assert(..)
assert := assert.New(t)
// этот url должен присутствовать в weed
url := "http://192.168.0.105:9091/1,10040dd9712a6b.png"
// формируем полное имя файла с путем.
filename := filepath.Join(tmpdir, "1,10040dd9712a6b.png")
// собственно загрузка файла
err := download(url, filename)
assert.Nil(err)
assert.FileExists(filename)
}
// Работает, но не понятно как интерпретировать результаты
func BenchmarkDownload(b *testing.B) {
seadb, err := NewSeaDB(db)
if err != nil {
panic(err)
}
urls, err := seadb.SmallGifs()
if err != nil {
panic(err)
}
for _, url := range urls {
filename, err := getFilename(url)
if err != nil {
panic(err)
}
err = download(url, filepath.Join("test", filename))
if err != nil {
panic(err)
}
}
}

@ -0,0 +1,102 @@
package pic
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
//"log"
"github.com/linxGnu/goseaweedfs"
)
var sw *goseaweedfs.Seaweed
func init() {
masterURL := "http://192.168.0.105:9333"
filer := []string{}
sw, _ = goseaweedfs.NewSeaweed(
masterURL, // master server
filer, // ну нету у меня filer
8096, // размер chunk
&http.Client{Timeout: 5 * time.Minute})
// просто проверим, что weed запущен
_, err := sw.Status()
if err != nil {
panic(err)
}
}
// загружает файл filename на weed сервер и возвращает url загруженного файла
func upload(filename string) (string, error) {
fid, err := sw.Submit(filename, "", "")
if err != nil {
return "", err
}
return fid.FileURL, nil
}
// удаляет файл из weed
func delete(url string) error {
fid, err := getFID(url)
if err != nil {
return err
}
err = sw.DeleteFile(fid, nil)
return err
}
// загружает файл из weed в filename
// filename должен содержать полный путь
func download(url string, filename string) error {
fid, err := getFID(url)
if err != nil {
return err
}
_, err = sw.Download(fid, nil, func(r io.Reader) (err error) {
data, err := io.ReadAll(r)
if err != nil {
return err
}
file, err := os.OpenFile(filename, os.O_CREATE, 0666)
if err != nil {
return err
}
_, err = file.Write(data)
if err != nil {
return err
}
err = file.Close()
return err
})
return err
}
func getFID(url string) (string, error) {
items := strings.Split(url, "/")
if len(items) < 2 {
return "", fmt.Errorf("url должен быть в формате: <http://address/fid.ext>")
}
return items[len(items)-1], nil
}
// возвращает имя файла из url без номера volume
// т.е. http://192.168.0.105:9090/1,15df3bf5a2b6cb => 15df3bf5a2b6cb
func getFilename(url string) (string, error) {
fn, err := getFID(url)
if err != nil {
return "", err
}
items := strings.Split(fn, ",")
return items[len(items)-1], nil
}

@ -0,0 +1,40 @@
package pic
import (
//"log"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUpload(t *testing.T) {
// проверка на пустой файл
url, err := upload("")
assert.NotNil(t, err)
assert.Equal(t, url, "", "должен быть пустым")
// правильный файл, ошибок не должно быть
url, err = upload("weed_test.go")
assert.Nil(t, err)
//log.Printf("url: %s\n", url)
// проверка на обработку ошибок (возможно перепутал url и fid)
err = delete("4,23456")
assert.NotNil(t, err)
// проверка на удаление корректного файла (url)
err = delete(url)
assert.Nil(t, err)
}
func TestGetFID(t *testing.T) {
fn, err := getFID("http://192.168.0.105:9091/6,23441c89622148.gif")
assert.Nil(t, err)
assert.Equal(t, fn, "6,23441c89622148.gif", "должны совпадать")
}
func TestGetFilename(t *testing.T) {
fn, err := getFilename("http://192.168.0.105:9091/6,23441c89622148.gif")
assert.Nil(t, err)
assert.Equal(t, fn, "23441c89622148.gif", "должны совпадать")
}
Loading…
Cancel
Save