Commit 29965d93 authored by Christian Pointner's avatar Christian Pointner
Browse files

major: refactoring, renamed group(s) to show(s) in backend

parent a825e1d9
......@@ -76,59 +76,59 @@ func InstallHTTPHandler(r *mux.Router, st *store.Store, im *importer.Importer, i
// TODO: add authentication middleware
// Groups
groupsHandler := make(handlers.MethodHandler)
groupsHandler["GET"] = api.ListGroups()
r.Handle("/groups", groupsHandler)
// Shows
showsHandler := make(handlers.MethodHandler)
showsHandler["GET"] = api.ListShows()
r.Handle("/shows", showsHandler)
groupHandler := make(handlers.MethodHandler)
groupHandler["POST"] = api.CreateGroup()
r.Handle("/groups/{group-id}", groupHandler)
showHandler := make(handlers.MethodHandler)
showHandler["POST"] = api.CreateShow()
r.Handle("/shows/{show-id}", showHandler)
// Files
filesHandler := make(handlers.MethodHandler)
filesHandler["GET"] = api.ListFilesOfGroup()
filesHandler["POST"] = api.CreateFileForGroup()
r.Handle("/groups/{group-id}/files", filesHandler)
filesHandler["GET"] = api.ListFilesOfShow()
filesHandler["POST"] = api.CreateFileForShow()
r.Handle("/shows/{show-id}/files", filesHandler)
fileHandler := make(handlers.MethodHandler)
fileHandler["GET"] = api.ReadFileOfGroup()
fileHandler["PATCH"] = api.PatchFileOfGroup()
fileHandler["DELETE"] = api.DeleteFileOfGroup()
r.Handle("/groups/{group-id}/files/{file-id}", fileHandler)
fileHandler["GET"] = api.ReadFileOfShow()
fileHandler["PATCH"] = api.PatchFileOfShow()
fileHandler["DELETE"] = api.DeleteFileOfShow()
r.Handle("/shows/{show-id}/files/{file-id}", fileHandler)
usageHandler := make(handlers.MethodHandler)
usageHandler["GET"] = api.ReadUsageOfFile()
r.Handle("/groups/{group-id}/files/{file-id}/usage", usageHandler)
r.Handle("/shows/{show-id}/files/{file-id}/usage", usageHandler)
importHandler := make(handlers.MethodHandler)
importHandler["GET"] = api.ReadImportOfFile()
importHandler["DELETE"] = api.CancelImportOfFile()
r.Handle("/groups/{group-id}/files/{file-id}/import", importHandler)
r.Handle("/shows/{show-id}/files/{file-id}/import", importHandler)
uploadHandler := make(handlers.MethodHandler)
// TODO: distignuish between flow.js and simple upload using the content type?!?
uploadHandler["PUT"] = api.UploadFileSimple()
uploadHandler["POST"] = api.UploadFileFlowJS()
uploadHandler["GET"] = api.TestFileFlowJS()
r.Handle("/groups/{group-id}/files/{file-id}/upload", uploadHandler)
r.Handle("/shows/{show-id}/files/{file-id}/upload", uploadHandler)
// Imports
importsHandler := make(handlers.MethodHandler)
importsHandler["GET"] = api.ListImportsOfGroup()
r.Handle("/groups/{group-id}/imports", importsHandler)
importsHandler["GET"] = api.ListImportsOfShow()
r.Handle("/shows/{show-id}/imports", importsHandler)
// Playlists
playlistsHandler := make(handlers.MethodHandler)
playlistsHandler["GET"] = api.ListPlaylistsOfGroup()
playlistsHandler["POST"] = api.CreatePlaylistForGroup()
r.Handle("/groups/{group-id}/playlists", playlistsHandler)
playlistsHandler["GET"] = api.ListPlaylistsOfShow()
playlistsHandler["POST"] = api.CreatePlaylistForShow()
r.Handle("/shows/{show-id}/playlists", playlistsHandler)
playlistHandler := make(handlers.MethodHandler)
playlistHandler["GET"] = api.ReadPlaylistOfGroup()
playlistHandler["PUT"] = api.UpdatePlaylistOfGroup()
playlistHandler["DELETE"] = api.DeletePlaylistOfGroup()
r.Handle("/groups/{group-id}/playlists/{playlist-id}", playlistHandler)
playlistHandler["GET"] = api.ReadPlaylistOfShow()
playlistHandler["PUT"] = api.UpdatePlaylistOfShow()
playlistHandler["DELETE"] = api.DeletePlaylistOfShow()
r.Handle("/shows/{show-id}/playlists/{playlist-id}", playlistHandler)
// Index
indexHandler := make(handlers.MethodHandler)
......
......@@ -36,11 +36,11 @@ import (
"gitlab.servus.at/autoradio/tank/store"
)
func (api *API) ListFilesOfGroup() http.Handler {
func (api *API) ListFilesOfShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
// TODO: implement pagination
files, err := api.store.ListFiles(vars["group-id"])
files, err := api.store.ListFiles(vars["show-id"])
if err != nil {
sendError(w, err)
return
......@@ -49,7 +49,7 @@ func (api *API) ListFilesOfGroup() http.Handler {
})
}
func (api *API) CreateFileForGroup() http.Handler {
func (api *API) CreateFileForShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
request := &FileCreateRequest{}
......@@ -78,17 +78,17 @@ func (api *API) CreateFileForGroup() http.Handler {
return
}
group := vars["group-id"]
show := vars["show-id"]
file := &store.File{}
file.Source.URI = request.SourceURI
if file, err = api.store.CreateFile(group, *file); err != nil {
if file, err = api.store.CreateFile(show, *file); err != nil {
sendError(w, err)
return
}
user := "anonymous" // TODO: get this from auth middleware
refID := "" // TODO: get this from query paremeter
job, err := api.importer.CreateJob(group, file.ID, *srcURI, user, refID)
job, err := api.importer.CreateJob(show, file.ID, *srcURI, user, refID)
if err != nil {
// shall we remove the file here... thinking...
sendError(w, err)
......@@ -110,7 +110,7 @@ func (api *API) CreateFileForGroup() http.Handler {
case "done":
<-job.Done()
}
if file, err = api.store.GetFile(group, file.ID); err != nil {
if file, err = api.store.GetFile(show, file.ID); err != nil {
sendError(w, err)
return
}
......@@ -118,7 +118,7 @@ func (api *API) CreateFileForGroup() http.Handler {
})
}
func (api *API) ReadFileOfGroup() http.Handler {
func (api *API) ReadFileOfShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := idFromString(vars["file-id"])
......@@ -126,7 +126,7 @@ func (api *API) ReadFileOfGroup() http.Handler {
sendWebResponse(w, http.StatusBadRequest, ErrorResponse{Error: "invalid file-id: " + err.Error()})
return
}
file, err := api.store.GetFile(vars["group-id"], id)
file, err := api.store.GetFile(vars["show-id"], id)
if err != nil {
sendError(w, err)
return
......@@ -135,7 +135,7 @@ func (api *API) ReadFileOfGroup() http.Handler {
})
}
func (api *API) PatchFileOfGroup() http.Handler {
func (api *API) PatchFileOfShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := idFromString(vars["file-id"])
......@@ -148,7 +148,7 @@ func (api *API) PatchFileOfGroup() http.Handler {
sendWebResponse(w, http.StatusBadRequest, ErrorResponse{Error: "error decoding request: " + err.Error()})
return
}
file, err := api.store.UpdateFileMetadata(vars["group-id"], id, data)
file, err := api.store.UpdateFileMetadata(vars["show-id"], id, data)
if err != nil {
sendError(w, err)
return
......@@ -157,7 +157,7 @@ func (api *API) PatchFileOfGroup() http.Handler {
})
}
func (api *API) DeleteFileOfGroup() http.Handler {
func (api *API) DeleteFileOfShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := idFromString(vars["file-id"])
......@@ -165,10 +165,10 @@ func (api *API) DeleteFileOfGroup() http.Handler {
sendWebResponse(w, http.StatusBadRequest, ErrorResponse{Error: "invalid file-id: " + err.Error()})
return
}
if job, err := api.importer.GetJob(vars["group-id"], id); err != importer.ErrNotFound {
if job, err := api.importer.GetJob(vars["show-id"], id); err != importer.ErrNotFound {
job.Cancel()
}
if err = api.store.DeleteFile(vars["group-id"], id); err != nil {
if err = api.store.DeleteFile(vars["show-id"], id); err != nil {
sendError(w, err)
return
}
......@@ -185,7 +185,7 @@ func (api *API) ReadUsageOfFile() http.Handler {
return
}
result := FileUsageListing{}
if result.Usage.Playlists, err = api.store.GetFileUsage(vars["group-id"], id); err != nil {
if result.Usage.Playlists, err = api.store.GetFileUsage(vars["show-id"], id); err != nil {
sendError(w, err)
return
}
......
......@@ -30,10 +30,10 @@ import (
"github.com/gorilla/mux"
)
func (api *API) ListImportsOfGroup() http.Handler {
func (api *API) ListImportsOfShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
jobs, err := api.importer.ListJobs(vars["group-id"])
jobs, err := api.importer.ListJobs(vars["show-id"])
if err != nil {
sendError(w, err)
return
......@@ -62,7 +62,7 @@ func (api *API) ReadImportOfFile() http.Handler {
return
}
job, err := api.importer.GetJob(vars["group-id"], id)
job, err := api.importer.GetJob(vars["show-id"], id)
if err != nil {
// TODO: return import info from store if err == ErrNotFound
sendError(w, err)
......@@ -87,7 +87,7 @@ func (api *API) CancelImportOfFile() http.Handler {
sendWebResponse(w, http.StatusBadRequest, ErrorResponse{Error: "invalid file-id: " + err.Error()})
return
}
job, err := api.importer.GetJob(vars["group-id"], id)
job, err := api.importer.GetJob(vars["show-id"], id)
if err != nil {
sendError(w, err)
return
......
......@@ -32,11 +32,11 @@ import (
"gitlab.servus.at/autoradio/tank/store"
)
func (api *API) ListPlaylistsOfGroup() http.Handler {
func (api *API) ListPlaylistsOfShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
// TODO: implement pagination
playlists, err := api.store.ListPlaylists(vars["group-id"])
playlists, err := api.store.ListPlaylists(vars["show-id"])
if err != nil {
sendError(w, err)
return
......@@ -45,7 +45,7 @@ func (api *API) ListPlaylistsOfGroup() http.Handler {
})
}
func (api *API) CreatePlaylistForGroup() http.Handler {
func (api *API) CreatePlaylistForShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playlist := &store.Playlist{}
......@@ -54,7 +54,7 @@ func (api *API) CreatePlaylistForGroup() http.Handler {
sendWebResponse(w, http.StatusBadRequest, ErrorResponse{Error: "error decoding playlist: " + err.Error()})
return
}
if playlist, err = api.store.CreatePlaylist(vars["group-id"], *playlist); err != nil {
if playlist, err = api.store.CreatePlaylist(vars["show-id"], *playlist); err != nil {
sendError(w, err)
return
}
......@@ -62,7 +62,7 @@ func (api *API) CreatePlaylistForGroup() http.Handler {
})
}
func (api *API) ReadPlaylistOfGroup() http.Handler {
func (api *API) ReadPlaylistOfShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := idFromString(vars["playlist-id"])
......@@ -70,7 +70,7 @@ func (api *API) ReadPlaylistOfGroup() http.Handler {
sendWebResponse(w, http.StatusBadRequest, ErrorResponse{Error: "invalid playlist-id: " + err.Error()})
return
}
playlist, err := api.store.GetPlaylist(vars["group-id"], id)
playlist, err := api.store.GetPlaylist(vars["show-id"], id)
if err != nil {
sendError(w, err)
return
......@@ -79,7 +79,7 @@ func (api *API) ReadPlaylistOfGroup() http.Handler {
})
}
func (api *API) UpdatePlaylistOfGroup() http.Handler {
func (api *API) UpdatePlaylistOfShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := idFromString(vars["playlist-id"])
......@@ -92,7 +92,7 @@ func (api *API) UpdatePlaylistOfGroup() http.Handler {
sendWebResponse(w, http.StatusBadRequest, ErrorResponse{Error: "error decoding playlist: " + err.Error()})
return
}
if playlist, err = api.store.UpdatePlaylist(vars["group-id"], id, *playlist); err != nil {
if playlist, err = api.store.UpdatePlaylist(vars["show-id"], id, *playlist); err != nil {
sendError(w, err)
return
}
......@@ -100,7 +100,7 @@ func (api *API) UpdatePlaylistOfGroup() http.Handler {
})
}
func (api *API) DeletePlaylistOfGroup() http.Handler {
func (api *API) DeletePlaylistOfShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := idFromString(vars["playlist-id"])
......@@ -108,7 +108,7 @@ func (api *API) DeletePlaylistOfGroup() http.Handler {
sendWebResponse(w, http.StatusBadRequest, ErrorResponse{Error: "invalid playlist-id: " + err.Error()})
return
}
if err = api.store.DeletePlaylist(vars["group-id"], id); err != nil {
if err = api.store.DeletePlaylist(vars["show-id"], id); err != nil {
sendError(w, err)
return
}
......
......@@ -30,27 +30,27 @@ import (
"github.com/gorilla/mux"
)
func (api *API) ListGroups() http.Handler {
func (api *API) ListShows() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
groups, err := api.store.ListGroups()
shows, err := api.store.ListShows()
if err != nil {
sendWebResponse(w, http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
}
// TODO: add all groups that are not present in the store but are accessable to the user
// TODO: add all shows that are not present in the store but are accessable to the user
// accroding to the auth info we got.
sendWebResponse(w, http.StatusOK, GroupsListing{groups})
sendWebResponse(w, http.StatusOK, ShowsListing{shows})
})
}
func (api *API) CreateGroup() http.Handler {
func (api *API) CreateShow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO: implement clone depending on query parameter
vars := mux.Vars(r)
group, err := api.store.CreateGroup(vars["group-id"])
show, err := api.store.CreateShow(vars["show-id"])
if err != nil {
sendError(w, err)
return
}
sendWebResponse(w, http.StatusCreated, group)
sendWebResponse(w, http.StatusCreated, show)
})
}
......@@ -51,7 +51,7 @@ func (api *API) UploadFileSimple() http.Handler {
sendWebResponse(w, http.StatusBadRequest, ErrorResponse{Error: "invalid file-id: " + err.Error()})
return
}
job, err := api.importer.GetJob(vars["group-id"], id)
job, err := api.importer.GetJob(vars["show-id"], id)
if err != nil {
sendError(w, err)
return
......@@ -382,7 +382,7 @@ func (api *API) UploadFileFlowJS() http.Handler {
return
}
job, err := api.importer.GetJob(vars["group-id"], id)
job, err := api.importer.GetJob(vars["show-id"], id)
if err != nil {
sendError(w, err)
return
......@@ -445,7 +445,7 @@ func (api *API) TestFileFlowJS() http.Handler {
return
}
job, err := api.importer.GetJob(vars["group-id"], id)
job, err := api.importer.GetJob(vars["show-id"], id)
if err != nil {
sendError(w, err)
return
......
......@@ -50,9 +50,9 @@ type APIListing struct {
Endpoints map[string]*APIEndpoint `json:"endpoints"`
}
// Groups
type GroupsListing struct {
Groups store.Groups `json:"results"`
// Shows
type ShowsListing struct {
Shows store.Shows `json:"results"`
}
// Imports
......
......@@ -88,26 +88,6 @@ func openStoreAndImporter(conf *Config) (st *store.Store, im *importer.Importer,
return
}
func cmdTest(c *cli.Context) error {
conf, err := ReadConfig(c.GlobalString("config"))
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
st, err := store.NewStore(conf.Store)
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
groups, err := st.ListGroups()
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
infoLog.Printf("groups: %v", groups)
return cli.NewExitError("", 0)
}
func cmdRun(c *cli.Context) error {
infoLog.Println("starting...")
......@@ -189,11 +169,6 @@ func main() {
Usage: "initialize the store",
Action: cmdInit,
},
{
Name: "test",
Usage: "run some tests",
Action: cmdTest,
},
{
Name: "run",
Usage: "run the daemon",
......
......@@ -2,7 +2,7 @@
## Background
`tank` stores groups using it's name as primary key. Since this is a string it
`tank` stores shows using it's name as primary key. Since this is a string it
by default is mapped to a `VARCHAR(255)` inside the database.
Starting with Debian 9 (stretch) MariaDB is installed instead of mySQL. Debian also
switched to `utf8mb4` as the default character set in an attempt to support Emoji's
......@@ -42,7 +42,7 @@ innodb_default_row_format = dynamic
* [Percona Server](https://www.percona.com/software/mysql-database/percona-server) is drop-in replacement for mySQL
* If for some reason none of the above is applicable as a last resort you can apply the patch below.
This limits the length for group names to 191 characters which is just short enough to not exceed the limit.
This limits the length for show names to 191 characters which is just short enough to not exceed the limit.
```patch
diff --git a/store/types.go b/store/types.go
......@@ -50,9 +50,9 @@ index 8576c7b..df99125 100644
--- a/store/types.go
+++ b/store/types.go
@@ -69,7 +69,7 @@ func (e ErrInvalidMetadataField) Error() string {
//******* Groups
//******* Shows
type Group struct {
type Show struct {
- Name string `json:"name" gorm:"primary_key"`
+ Name string `json:"name" gorm:"primary_key;size:191"`
CreatedAt time.Time `json:"created"`
......@@ -62,18 +62,18 @@ index 8576c7b..df99125 100644
ID uint64 `json:"id" gorm:"primary_key"`
CreatedAt time.Time `json:"created"`
UpdatedAt time.Time `json:"updated"`
- GroupName string `json:"group" gorm:"not null;index"`
+ GroupName string `json:"group" gorm:"not null;index;size:191"`
Group Group `json:"-" gorm:"association_foreignkey:Name"`
- ShowName string `json:"show" gorm:"not null;index"`
+ ShowName string `json:"show" gorm:"not null;index;size:191"`
Show Show `json:"-" gorm:"association_foreignkey:Name"`
Source FileSource `json:"source" gorm:"embedded;embedded_prefix:source__"`
Metadata FileMetadata `json:"metadata" gorm:"embedded;embedded_prefix:metadata__"`
@@ -194,7 +194,7 @@ type Playlist struct {
ID uint64 `json:"id" gorm:"primary_key"`
CreatedAt time.Time `json:"created"`
UpdatedAt time.Time `json:"updated"`
- GroupName string `json:"group" gorm:"not null;index"`
+ GroupName string `json:"group" gorm:"not null;index;size:191"`
Group Group `json:"-" gorm:"association_foreignkey:Name"`
- ShowName string `json:"show" gorm:"not null;index"`
+ ShowName string `json:"show" gorm:"not null;index;size:191"`
Show Show `json:"-" gorm:"association_foreignkey:Name"`
Entries []PlaylistEntry `json:"entries,omitempty"`
}
```
use tank;
show tables;
select * from __migrations__;
describe groups;
show index from groups;
describe shows;
show index from shows;
describe files;
show index from files;
describe playlists;
......
use tank;
/* should succeed */
insert into groups (name) values ('test');
insert into shows (name) values ('test');
/* should fail */
insert into files (size) values(100);
insert into files (group_name, size) values('invalid', 101);
insert into files (show_name, size) values('invalid', 101);
/* should succeed */
insert into files (group_name, size) values('test', 101);
insert into files (group_name, size) values('test', 102);
insert into files (show_name, size) values('test', 101);
insert into files (show_name, size) values('test', 102);
select * from files;
......@@ -17,7 +17,7 @@ select * from files;
insert into playlists (created_at) values ('2018-06-17 02:38:17');
/* should succeed */
insert into playlists (created_at, group_name) values('2018-06-17 02:38:17', 'test');
insert into playlists (created_at, show_name) values('2018-06-17 02:38:17', 'test');
/* should fail */
......
\c tank
\dt
select * from __migrations__;
\d groups;
\d shows;
\d files;
\d playlists;
\d playlist_entries;
\c tank
/* should succeed */
insert into groups (name) values ('test');
insert into shows (name) values ('test');
/* should fail */
insert into files (size) values(100);
insert into files (group_name, size) values('invalid', 101);
insert into files (show_name, size) values('invalid', 101);
/* should succeed */
insert into files (group_name, size) values('test', 101);
insert into files (group_name, size) values('test', 102);
insert into files (show_name, size) values('test', 101);
insert into files (show_name, size) values('test', 102);
select * from files;
......@@ -17,7 +17,7 @@ select * from files;
insert into playlists (created_at) values ('2018-06-17 02:38:17');
/* should succeed */
insert into playlists (created_at, group_name) values('2018-06-17 02:38:17', 'test');
insert into playlists (created_at, show_name) values('2018-06-17 02:38:17', 'test');
/* should fail */
......
......@@ -7,7 +7,10 @@ export AURA_TANK_LISTEN=127.0.0.1:8080
export STORE_PATH="/run/user/${UID}/aura-tank"
mkdir -p "${STORE_PATH}"
export OIDC_CLIENT_ID="693347"
export OIDC_CLIENT_SECRET="f9475d777a2180f71c02cb0d0d56839f8ee6e66e1e2ef5df6c55451b"
#export OIDC_CLIENT_ID="693347"
#export OIDC_CLIENT_SECRET="f9475d777a2180f71c02cb0d0d56839f8ee6e66e1e2ef5df6c55451b"
export OIDC_CLIENT_ID="106243"
export OIDC_CLIENT_SECRET="942234084668700fd3c86227a04be646aa9323afa7f69f913d8abe99"
"$BASE_D/tank" --config "$BASE_D/contrib/sample-cfg.yaml" run
......@@ -55,7 +55,7 @@ func (job *Job) copyFromSource(w io.Writer, done chan<- copyFromSourceResult) {
//hashStr := "blake2b_256:" + base64.URLEncoding.EncodeToString(hash.Sum(nil))
hashStr := "sha256:" + hex.EncodeToString(hash.Sum(nil))
job.im.dbgLog.Printf("fetch(): done copying %d bytes from source (%s)", written, hashStr)
_, err = job.im.store.UpdateFileSourceHash(job.Group, job.ID, hashStr)
_, err = job.im.store.UpdateFileSourceHash(job.Show, job.ID, hashStr)
done <- copyFromSourceResult{err, hashStr}
}
......
......@@ -48,11 +48,11 @@ type Importer struct {
httpC *http.Client
}
func (im *Importer) ListJobs(group string) (Jobs, error) {
return im.jobs.ListJobs(group), nil // for now error is always nil but this might change later
func (im *Importer) ListJobs(show string) (Jobs, error) {
return im.jobs.ListJobs(show), nil // for now error is always nil but this might change later
}
func (im *Importer) CreateJob(group string, id uint64, src url.URL, user, refID string) (*Job, error) {
func (im *Importer) CreateJob(show string, id uint64, src url.URL, user, refID string) (*Job, error) {
switch src.Scheme {
case SourceSchemeUpload:
case SourceSchemeFake:
......@@ -62,7 +62,7 @@ func (im *Importer) CreateJob(group string, id uint64, src url.URL, user, refID