M accounts/utils.go => accounts/utils.go +2 -2
@@ 29,7 29,7 @@ func SendVerificationEmail(ctx context.Context, conf *accounts.Confirmation, use
}
// NOTE: improve this
lt := localizer.GetLocalizer("")
- pd := localizer.NewPageData(lt.Translate("Links - Confirm account email"))
+ pd := localizer.NewPageData(lt.Translate("LinkTaco - Confirm account email"))
pd.Data["please_confirm"] = lt.Translate("Please confirm your account")
pd.Data["to_confirm"] = lt.Translate("To confirm your email address and complete your Links registration, please click the link below.")
pd.Data["confirm"] = lt.Translate("Confirm")
@@ 88,6 88,6 @@ func ResendVerificationConf(ctx context.Context, user gobwebs.User) (bool, error
linkUser := user.(*models.User)
lt := localizer.GetLocalizer(linkUser.Settings.Account.DefaultLang)
- return true, fmt.Errorf(lt.Translate("We sent you a new confirmation email. Please click the link in that " +
+ return true, fmt.Errorf("%s", lt.Translate("We sent you a new confirmation email. Please click the link in that "+
"email to confirm your account."))
}
M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +9 -15
@@ 299,13 299,8 @@ func (r *mutationResolver) AddLink(ctx context.Context, input *model.LinkInput)
}
}
- baseURL, err := url.Parse(input.URL)
- if err != nil {
- return nil, fmt.Errorf("%s", lt.Translate("Error parsin url: %s", err))
- }
- baseURL.Fragment = ""
BaseURL := &models.BaseURL{
- URL: baseURL.String(),
+ URL: links.StripURLFragment(input.URL),
}
err = BaseURL.Store(ctx)
if err != nil {
@@ 313,7 308,9 @@ func (r *mutationResolver) AddLink(ctx context.Context, input *model.LinkInput)
}
if input.Override == nil || (*input.Override && input.ParseBaseURL != nil && *input.ParseBaseURL) {
- srv.QueueTask("general", core.ParseBaseURLTask(srv, BaseURL))
+ if visibility == models.OrgLinkVisibilityPublic {
+ srv.QueueTask("general", core.ParseBaseURLTask(srv, BaseURL))
+ }
}
userID := int(user.ID)
@@ 472,13 469,8 @@ func (r *mutationResolver) UpdateLink(ctx context.Context, input *model.UpdateLi
WithCode(valid.ErrValidationCode)
return nil, nil
}
- baseURL, err := url.Parse(*input.URL)
- if err != nil {
- return nil, fmt.Errorf("%s", lt.Translate("Error parsin url: %s", err))
- }
- baseURL.Fragment = ""
BaseURL := &models.BaseURL{
- URL: baseURL.String(),
+ URL: links.StripURLFragment(*input.URL),
}
err = BaseURL.Store(ctx)
if err != nil {
@@ 487,7 479,9 @@ func (r *mutationResolver) UpdateLink(ctx context.Context, input *model.UpdateLi
orgLink.BaseURLID = sql.NullInt64{Valid: true, Int64: int64(BaseURL.ID)}
orgLink.URL = *input.URL
- srv.QueueTask("general", core.ParseBaseURLTask(srv, BaseURL))
+ if input.Visibility != nil && string(*input.Visibility) == models.OrgLinkVisibilityPublic {
+ srv.QueueTask("general", core.ParseBaseURLTask(srv, BaseURL))
+ }
}
if input.Visibility != nil {
@@ 812,7 806,7 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput)
Title: input.Title,
OrgID: org.ID,
Description: gcore.StripHtmlTags(input.Description),
- BaseURLID: sql.NullInt64{Valid: BaseURL.ID > 0, Int64: int64(BaseURL.ID)},
+ BaseURLID: sql.NullInt64{Valid: true, Int64: int64(BaseURL.ID)},
Visibility: string(input.Visibility),
Starred: input.Starred,
URL: noteURL,
M cmd/api/main.go => cmd/api/main.go +1 -1
@@ 82,7 82,7 @@ func run() error {
return fmt.Errorf("unable to load storage service: %v", err)
}
- eSize, gSize, err := cmd.LoadWorkerQueueSizes(config)
+ eSize, gSize, _, err := cmd.LoadWorkerQueueSizes(config)
if err != nil {
return err
}
M cmd/global.go => cmd/global.go +1 -1
@@ 47,7 47,7 @@ func RunMigrations(t *testing.T, db *sql.DB) {
func GetWorkerPoolCount(queue *work.Queue) int {
var pSize int
switch queue.Name() {
- case "general":
+ case "general", "import":
pSize = 10
case "invoice":
pSize = 3
M cmd/links/main.go => cmd/links/main.go +3 -2
@@ 160,7 160,7 @@ func run() error {
return fmt.Errorf("Unknown storage service configured")
}
- eSize, gSize, err := cmd.LoadWorkerQueueSizes(config)
+ eSize, gSize, iSize, err := cmd.LoadWorkerQueueSizes(config)
if err != nil {
return err
}
@@ 183,6 183,7 @@ func run() error {
sq := email.NewServiceQueue(e.Logger, esvc, eq, &mailChecker)
wq := work.NewQueue("general", gSize)
wqi := work.NewQueue("invoice", gSize)
+ imq := work.NewQueue("import", iSize)
mwConf := &server.MiddlewareConfig{
Sessions: true,
@@ 216,7 217,7 @@ func run() error {
WithStorage(storesvc).
DefaultMiddlewareWithConfig(mwConf).
SetWorkerPoolFunc(cmd.GetWorkerPoolCount).
- WithQueues(eq, wq, wqi).
+ WithQueues(eq, wq, wqi, imq).
WithMiddleware(
database.Middleware(db),
core.RemoteIPMiddleware,
M cmd/list/main.go => cmd/list/main.go +1 -1
@@ 59,7 59,7 @@ func run() error {
}
}
- eSize, gSize, err := cmd.LoadWorkerQueueSizes(config)
+ eSize, gSize, _, err := cmd.LoadWorkerQueueSizes(config)
if err != nil {
return err
}
M cmd/server.go => cmd/server.go +6 -6
@@ 208,22 208,22 @@ func LoadAutoTLS(config *config.Config, db *sql.DB, service string) *autocert.Ma
}
// LoadWorkerQueueSizes ...
-func LoadWorkerQueueSizes(config *config.Config) (int, int, error) {
+func LoadWorkerQueueSizes(config *config.Config) (int, int, int, error) {
var (
- eSize, gSize int = 512, 512
- err error
+ eSize, gSize, iSize int = 512, 512, 1024
+ err error
)
if tStr, ok := config.File.Get("links", "email-queue-size"); ok {
eSize, err = strconv.Atoi(tStr)
if err != nil {
- return eSize, gSize, fmt.Errorf("links:email-queue-size invalid")
+ return eSize, gSize, iSize, fmt.Errorf("links:email-queue-size invalid")
}
}
if tStr, ok := config.File.Get("links", "general-queue-size"); ok {
gSize, err = strconv.Atoi(tStr)
if err != nil {
- return eSize, gSize, fmt.Errorf("links:general-queue-size invalid")
+ return eSize, gSize, iSize, fmt.Errorf("links:general-queue-size invalid")
}
}
- return eSize, gSize, nil
+ return eSize, gSize, iSize, nil
}
M cmd/short/main.go => cmd/short/main.go +1 -1
@@ 57,7 57,7 @@ func run() error {
}
}
- eSize, gSize, err := cmd.LoadWorkerQueueSizes(config)
+ eSize, gSize, _, err := cmd.LoadWorkerQueueSizes(config)
if err != nil {
return err
}
M config.example.ini => config.example.ini +9 -2
@@ 92,6 92,10 @@ ses-secret-key=YOUR_AWS_SECRET_KEY
# Defaults to: ./
root-directory=./
+# Value that can be used to set a custom temp directory.
+# Defaults to: /tmp
+tmp-directory=/tmp
+
# If storage is s3, set the appropriate variables here
# s3 works with any s3 compatible service (ie, minio, etc.)
endpoint=s3.amazonaws.com
@@ 182,12 186,15 @@ rate-limit-burst=40
# How long (in minutes) of inactivity does the limit record live for
rate-limit-expire=3
-# How many email queue workers. Defaults to 512
+# How large is the email queue buffer. Defaults to 512
email-queue-size = 512
-# How many general queue workers. Defaults to 512
+# How large is the general queue buffer. Defaults to 512
general-queue-size = 512
+# How large is the import queue buffer. Defaults to 1024
+import-queue-size = 1024
+
# How many short links can free accounts create per month
short-limit-month = 10
M core/import.go => core/import.go +111 -59
@@ 5,11 5,12 @@ import (
"database/sql"
"encoding/json"
"fmt"
+ "io"
"links"
"links/models"
- "mime/multipart"
"net/url"
"strings"
+ "time"
sq "github.com/Masterminds/squirrel"
"github.com/labstack/echo/v4"
@@ 17,6 18,7 @@ import (
"golang.org/x/net/html"
"netlandish.com/x/gobwebs/database"
"netlandish.com/x/gobwebs/server"
+ "netlandish.com/x/gobwebs/storage"
)
// Define import source, used in template and handler
@@ 201,7 203,7 @@ func (i importAdapter) GetType() int {
}
func processBaseURLs(obj importObj, tmpURLMap map[string]bool, urlList []string, c echo.Context) {
- importedURL, err := url.ParseRequestURI(obj.GetURL())
+ importedURL, err := url.Parse(obj.GetURL())
if err != nil {
return
}
@@ 216,25 218,33 @@ func processBaseURLs(obj importObj, tmpURLMap map[string]bool, urlList []string,
}
// Get a list of base url to make a query
// of existing ones
- urlList = append(urlList, obj.GetURL())
- tmpURLMap[obj.GetURL()] = true
+ strippedURL := links.StripURLFragment(obj.GetURL())
+ urlList = append(urlList, strippedURL)
+ tmpURLMap[strippedURL] = true
}
// Get a importAdapter with a list of pinBoardObj, HTMLObj and FirefoxObj
// and create the base url objects in the db
// returns a map containing the base url as index and its id as value
-func importBaseURLs(c echo.Context, objAdapter *importAdapter) (map[string]int, error) {
+func importBaseURLs(ctx context.Context, objAdapter *importAdapter) (map[string]int, error) {
urlList := make([]string, 0)
tmpURLMap := make(map[string]bool) // temporary map to store non existing url
baseURLMap := make(map[string]int)
+ srv := server.ForContext(ctx)
+
+ // TODO: Rethink this process. Total hack for the moment all to use a URL
+ // helper
+ gctx := &server.Context{
+ Server: srv,
+ }
switch objAdapter.GetType() {
case pinBoardType:
for _, obj := range objAdapter.GetPinBoards() {
- processBaseURLs(obj, tmpURLMap, urlList, c)
+ processBaseURLs(obj, tmpURLMap, urlList, gctx)
}
case htmlType:
for _, obj := range objAdapter.GetHTMLLinks() {
- processBaseURLs(obj, tmpURLMap, urlList, c)
+ processBaseURLs(obj, tmpURLMap, urlList, gctx)
}
}
@@ 242,7 252,6 @@ func importBaseURLs(c echo.Context, objAdapter *importAdapter) (map[string]int,
opts := &database.FilterOptions{
Filter: sq.Eq{"b.url": urlList},
}
- ctx := c.Request().Context()
baseURLs, err := models.GetBaseURLs(ctx, opts)
if err != nil {
return nil, err
@@ 265,13 274,14 @@ func importBaseURLs(c echo.Context, objAdapter *importAdapter) (map[string]int,
return nil, err
}
baseURLMap[baseURL.URL] = baseURL.ID
+ srv.QueueTask("import", ParseBaseURLTask(srv, baseURL))
}
return baseURLMap, nil
}
func processOrgLinks(obj importObj, baseURLMap map[string]int,
org *models.Organization, user *models.User, billEnabled bool) *models.OrgLink {
- baseID, ok := baseURLMap[obj.GetURL()]
+ baseID, ok := baseURLMap[links.StripURLFragment(obj.GetURL())]
if !ok {
return nil
}
@@ 368,65 378,91 @@ func importOrgLinks(ctx context.Context, objAdapter *importAdapter, baseURLMap m
return nil
}
-func ImportFromPinBoard(c echo.Context, src multipart.File,
+func getTmpFileStorage(ctx context.Context) storage.Service {
+ srv := server.ForContext(ctx)
+ tmpDir, ok := srv.Config.File.Get("storage", "tmp-directory")
+ if !ok {
+ tmpDir = "/tmp"
+ }
+ return storage.NewFileSystemService(tmpDir)
+}
+
+func ImportFromPinBoard(ctx context.Context, path string,
org *models.Organization, user *models.User) error {
- var pinBoardList []*pinBoardObj
- err := json.NewDecoder(src).Decode(&pinBoardList)
+ fs := getTmpFileStorage(ctx)
+ fobj, err := fs.GetObject(ctx, path)
if err != nil {
return err
}
- var listlen, start, end int
- step := 100
-
- adapter := &importAdapter{
- elementType: pinBoardType,
- start: start,
- end: end,
- pinBoards: pinBoardList,
+ dcode := json.NewDecoder(fobj.Content)
+ _, err = dcode.Token()
+ if err != nil {
+ return fmt.Errorf("Error parsing json: %w", err)
}
- gctx := c.(*server.Context)
- billEnabled := links.BillingEnabled(gctx.Server.Config)
+ var totalCount int
+ step := 100
+ srv := server.ForContext(ctx)
+ billEnabled := links.BillingEnabled(srv.Config)
- listlen = len(pinBoardList)
- for start < listlen {
- if end+step > listlen {
- end = listlen
- } else {
- end += step
- }
- adapter.start = start
- adapter.end = end
- baseURLMap, err := importBaseURLs(c, adapter)
- if err != nil {
- return err
+ for {
+ var pinBoardList []*pinBoardObj
+ for dcode.More() {
+ var pbObj *pinBoardObj
+ err := dcode.Decode(&pbObj)
+ if err != nil {
+ srv.Logger().Printf("Error decoding json object in pinboard import: %v", err)
+ continue
+ }
+ pinBoardList = append(pinBoardList, pbObj)
+ if len(pinBoardList) == step {
+ break
+ }
}
- err = importOrgLinks(
- c.Request().Context(),
- adapter,
- baseURLMap,
- org,
- user,
- billEnabled,
- )
- if err != nil {
- return err
- }
+ listlen := len(pinBoardList)
+ if listlen > 0 {
+ adapter := &importAdapter{
+ elementType: pinBoardType,
+ start: 0,
+ end: listlen,
+ pinBoards: pinBoardList,
+ }
- start += step
+ totalCount += listlen
+
+ baseURLMap, err := importBaseURLs(ctx, adapter)
+ if err != nil {
+ return err
+ }
+
+ err = importOrgLinks(
+ ctx,
+ adapter,
+ baseURLMap,
+ org,
+ user,
+ billEnabled,
+ )
+ if err != nil {
+ return err
+ }
+ time.Sleep(3 * time.Second) // Let the parse url workers catch up
+ } else {
+ break // No more items to process
+ }
}
- if listlen > 0 {
+ if totalCount > 0 {
mdata := make(map[string]any)
mdata["org_id"] = org.ID
err := models.RecordAuditLog(
- c.Request().Context(),
+ ctx,
int(user.ID),
- c.RealIP(),
+ "internal task",
models.LOG_BOOKMARK_IMPORTED,
- fmt.Sprintf("Imported %d Pinboard bookmarks into organization %s.", listlen, org.Slug),
+ fmt.Sprintf("Imported %d Pinboard bookmarks into organization %s.", totalCount, org.Slug),
mdata,
)
if err != nil {
@@ 434,6 470,11 @@ func ImportFromPinBoard(c echo.Context, src multipart.File,
}
}
+ // XXX Pass some sort of flag to call this, in the future
+ //err = fs.DeleteObject(ctx, path)
+ //if err != nil {
+ // return err
+ //}
return nil
}
@@ 443,7 484,7 @@ type htmlObjData struct {
}
// Parse import html files. Used for chrome and safari
-func getLinksFromHTML(src multipart.File) map[string]htmlObjData {
+func getLinksFromHTML(src io.Reader) map[string]htmlObjData {
tkn := html.NewTokenizer(src)
vals := make(map[string]htmlObjData)
var (
@@ 500,9 541,14 @@ func getLinksFromHTML(src multipart.File) map[string]htmlObjData {
}
// Get an html
-func ImportFromHTML(c echo.Context, src multipart.File,
+func ImportFromHTML(ctx context.Context, path string,
org *models.Organization, user *models.User) error {
- linksSrc := getLinksFromHTML(src)
+ fs := getTmpFileStorage(ctx)
+ fobj, err := fs.GetObject(ctx, path)
+ if err != nil {
+ return err
+ }
+ linksSrc := getLinksFromHTML(fobj.Content)
var htmlList []*htmlObj
for i, v := range linksSrc {
l := &htmlObj{
@@ 523,8 569,8 @@ func ImportFromHTML(c echo.Context, src multipart.File,
htmlList: htmlList,
}
- gctx := c.(*server.Context)
- billEnabled := links.BillingEnabled(gctx.Server.Config)
+ srv := server.ForContext(ctx)
+ billEnabled := links.BillingEnabled(srv.Config)
listlen = len(htmlList)
for start < listlen {
@@ 535,13 581,13 @@ func ImportFromHTML(c echo.Context, src multipart.File,
}
adapter.start = start
adapter.end = end
- baseURLMap, err := importBaseURLs(c, adapter)
+ baseURLMap, err := importBaseURLs(ctx, adapter)
if err != nil {
return err
}
err = importOrgLinks(
- c.Request().Context(),
+ ctx,
adapter,
baseURLMap,
org,
@@ 559,9 605,9 @@ func ImportFromHTML(c echo.Context, src multipart.File,
mdata := make(map[string]any)
mdata["org_id"] = org.ID
err := models.RecordAuditLog(
- c.Request().Context(),
+ ctx,
int(user.ID),
- c.RealIP(),
+ "internal task",
models.LOG_BOOKMARK_IMPORTED,
fmt.Sprintf("Imported %d bookmarks into organization %s.", listlen, org.Slug),
mdata,
@@ 570,5 616,11 @@ func ImportFromHTML(c echo.Context, src multipart.File,
return err
}
}
+
+ // XXX Pass some sort of flag to call this, in the future
+ //err = fs.DeleteObject(ctx, path)
+ //if err != nil {
+ // return err
+ //}
return nil
}
M core/inputs.go => core/inputs.go +39 -18
@@ 4,13 4,16 @@ import (
"errors"
"fmt"
"links/internal/localizer"
- "mime/multipart"
"net/http"
"net/url"
"path/filepath"
"strings"
+ "time"
"github.com/labstack/echo/v4"
+ "github.com/segmentio/ksuid"
+ "netlandish.com/x/gobwebs/server"
+ "netlandish.com/x/gobwebs/storage"
"netlandish.com/x/gobwebs/validate"
)
@@ 162,9 165,9 @@ func (d *DomainForm) Validate(c echo.Context) error {
}
type ImportForm struct {
- File string `form:"file"`
- Origin int `form:"origin" validate:"oneof=0 1 2 3"`
- src multipart.File `form:"-"`
+ File string `form:"file"`
+ Origin int `form:"origin" validate:"oneof=0 1 2 3"`
+ FilePath string `form:"-"`
}
func (i *ImportForm) Validate(c echo.Context) error {
@@ 181,33 184,51 @@ func (i *ImportForm) Validate(c echo.Context) error {
return err
}
- f, err := c.FormFile("file")
+ lt := localizer.GetSessionLocalizer(c)
+ if i.Origin < pinBoardOrigin || i.Origin > firefoxOrigin {
+ err = fmt.Errorf("%s", lt.Translate("Invalid origin source for importer."))
+ return validate.InputErrors{"Origin": err}
+ }
+
+ file, hdr, err := c.Request().FormFile("file")
if err != nil {
if errors.Is(err, http.ErrMissingFile) {
- lt := localizer.GetSessionLocalizer(c)
- err = fmt.Errorf(lt.Translate("The file is required"))
+ err = fmt.Errorf("%s", lt.Translate("The file is required"))
return validate.InputErrors{"File": err}
}
return err
}
- src, err := f.Open()
- if err != nil {
- return err
- }
- defer src.Close()
- ext := strings.ToLower(filepath.Ext(f.Filename))
+ defer file.Close()
+
+ ext := strings.ToLower(filepath.Ext(hdr.Filename))
if i.Origin == safariOrigin || i.Origin == chromeOrigin || i.Origin == firefoxOrigin {
if ext != ".html" {
- lt := localizer.GetSessionLocalizer(c)
- err = fmt.Errorf(lt.Translate("The file submitted for this source should be html"))
+ err = fmt.Errorf("%s", lt.Translate("The file submitted for this source should be html"))
return validate.InputErrors{"File": err}
}
} else if i.Origin == pinBoardOrigin && ext != ".json" {
- lt := localizer.GetSessionLocalizer(c)
- err = fmt.Errorf(lt.Translate("The file submitted for this source should be json"))
+ err = fmt.Errorf("%s", lt.Translate("The file submitted for this source should be json"))
+ return validate.InputErrors{"File": err}
+ }
+
+ gctx := c.(*server.Context)
+ tmpDir, ok := gctx.Server.Config.File.Get("storage", "tmp-directory")
+ if !ok {
+ tmpDir = "/tmp"
+ }
+ path := fmt.Sprintf("import/%d/%s/%s%s",
+ gctx.User.GetID(),
+ time.Now().UTC().Format("2006-01-02"),
+ ksuid.New().String(),
+ ext,
+ )
+ fs := storage.NewFileSystemService(tmpDir)
+ err = fs.PutObject(c.Request().Context(), path, file)
+ if err != nil {
+ c.Logger().Printf("Error writing tmp file for user bookmark import: %v", err)
return validate.InputErrors{"File": err}
}
- i.src = src
+ i.FilePath = path
return nil
}
M core/processors.go => core/processors.go +71 -0
@@ 3,15 3,86 @@ package core
import (
"context"
"links"
+ "links/internal/localizer"
"links/models"
work "git.sr.ht/~sircmpwn/dowork"
+ "github.com/labstack/echo/v4"
"netlandish.com/x/gobwebs"
"netlandish.com/x/gobwebs/database"
+ "netlandish.com/x/gobwebs/email"
"netlandish.com/x/gobwebs/server"
"netlandish.com/x/gobwebs/timezone"
+ "netlandish.com/x/gobwebs/validate"
)
+// ImportBookmarksTask task to parse html title tags.
+func ImportBookmarksTask(c echo.Context, origin int, path string,
+ org *models.Organization, user *models.User) *work.Task {
+ lt := localizer.GetLocalizer("")
+ gctx := c.(*server.Context)
+ return work.NewTask(func(ctx context.Context) error {
+ ctx = server.ServerContext(ctx, gctx.Server)
+ ctx = database.Context(ctx, gctx.Server.DB)
+ ctx = timezone.Context(ctx, "UTC")
+
+ var err error
+ switch origin {
+ case pinBoardOrigin:
+ // Run task to import from Pinboard
+ err = ImportFromPinBoard(ctx, path, org, user)
+ default:
+ err = ImportFromHTML(ctx, path, org, user)
+ }
+
+ if err != nil {
+ return err
+ }
+
+ server := server.ForContext(ctx)
+ tmpl := server.Echo().Renderer.(*validate.Template).Templates()
+ tmap := email.TMap{
+ "email_import_complete": []string{
+ "email_import_complete_subject.txt",
+ "email_import_complete_text.txt",
+ "email_import_complete_html.html",
+ },
+ }
+ // NOTE: improve this
+ pd := localizer.NewPageData(lt.Translate("LinkTaco - Your bookmark import is complete"))
+ pd.Data["hi"] = lt.Translate("Hi there,")
+ pd.Data["complete"] = lt.Translate("Just wanted to let you know that your bookmark import has completed successfully!")
+ pd.Data["team"] = lt.Translate("- LinkTaco Team")
+
+ helper := email.NewHelper(server.Email, tmap, tmpl)
+ err = helper.Send(
+ "email_import_complete",
+ server.Config.DefaultFromEmail,
+ gctx.User.GetEmail(),
+ gobwebs.Map{"pd": pd},
+ )
+ if err != nil {
+ server.Logger().Printf("Error sending import complete email: %v", err)
+ }
+ return nil
+ }).Retries(3).Before(func(ctx context.Context, task *work.Task) {
+ gobwebs.TaskIDWork(task)
+ gctx.Server.Logger().Printf(
+ "Running task ImportBookmarksTask %s for the %d attempt.",
+ task.Metadata["id"], task.Attempts())
+ }).After(func(ctx context.Context, task *work.Task) {
+ if task.Result() == nil {
+ gctx.Server.Logger().Printf(
+ "Completed task ImportBookmarksTask %s after %d attempts.",
+ task.Metadata["id"], task.Attempts())
+ } else {
+ gctx.Server.Logger().Printf(
+ "Failed task ImportBookmarksTask %s after %d attempts: %v",
+ task.Metadata["id"], task.Attempts(), task.Result())
+ }
+ })
+}
+
// ParseBaseURLTask task to parse html title tags.
func ParseBaseURLTask(srv *server.Server, baseURL *models.BaseURL) *work.Task {
return work.NewTask(func(ctx context.Context) error {
M core/routes.go => core/routes.go +11 -13
@@ 3018,6 3018,12 @@ func (s *Service) ImportData(c echo.Context) error {
"isFree": org.IsFreeAccount(),
}
if req.Method == http.MethodPost {
+ // Set file limit to 200MB for import handler
+ err := c.Request().ParseMultipartForm(200 << 20)
+ if err != nil {
+ return fmt.Errorf("Unable to set file upload limit: %w", err)
+ }
+
if err := form.Validate(c); err != nil {
switch err.(type) {
case validate.InputErrors:
@@ 3028,22 3034,14 @@ func (s *Service) ImportData(c echo.Context) error {
return err
}
}
- switch form.Origin {
- case pinBoardOrigin:
- err = ImportFromPinBoard(c, form.src, org, user)
- case safariOrigin:
- err = ImportFromHTML(c, form.src, org, user)
- case chromeOrigin:
- err = ImportFromHTML(c, form.src, org, user)
- case firefoxOrigin:
- err = ImportFromHTML(c, form.src, org, user)
- default:
- // Should not be reached since this is validated in the form
- return fmt.Errorf("Invalid origin source for importer")
- }
+
+ err = gctx.Server.QueueTask("import",
+ ImportBookmarksTask(c, form.Origin, form.FilePath, org, user))
if err != nil {
return err
}
+ messages.Success(c, lt.Translate(
+ "Your bookmark import is being processed. We will notify you once it's complete."))
return c.Redirect(http.StatusMovedPermanently, c.Echo().Reverse("core:org_link_list", org.Slug))
}
M helpers.go => helpers.go +10 -0
@@ 1180,3 1180,13 @@ func SanitizeUTF8(input string) string {
}
return b.String()
}
+
+// StripURLFragment will simply return a URL without any fragment options
+func StripURLFragment(furl string) string {
+ baseURL, err := url.Parse(furl)
+ if err != nil {
+ return furl
+ }
+ baseURL.Fragment = ""
+ return baseURL.String()
+}
M models/base_url.go => models/base_url.go +4 -4
@@ 150,14 150,14 @@ func (b *BaseURL) Store(ctx context.Context) error {
ON CONFLICT (url) DO UPDATE
SET url = NULL
WHERE FALSE
- RETURNING id, url, created_on
+ RETURNING id, url, public_ready, created_on
)
- SELECT id, url, created_on FROM ins
+ SELECT id, url, public_ready, created_on FROM ins
UNION ALL
- SELECT id, url, created_on FROM base_urls
+ SELECT id, url, public_ready, created_on FROM base_urls
WHERE url = $1
LIMIT 1;`, b.URL, b.Hash)
- err := row.Scan(&b.ID, &b.URL, &b.CreatedOn)
+ err := row.Scan(&b.ID, &b.URL, &b.PublicReady, &b.CreatedOn)
if err != nil {
return err
}
A templates/email_import_complete_html.html => templates/email_import_complete_html.html +5 -0
@@ 0,0 1,5 @@
+{{template "email_base" .}}
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">{{.pd.Data.hi}}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">{{.pd.Data.complete}}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">{{.pd.Data.team}}</p>
+{{template "email_base_footer" .}}
A templates/email_import_complete_subject.txt => templates/email_import_complete_subject.txt +1 -0
@@ 0,0 1,1 @@
+{{.pd.Title}}
A templates/email_import_complete_text.txt => templates/email_import_complete_text.txt +6 -0
@@ 0,0 1,6 @@
+{{.pd.Data.hi}}
+
+{{.pd.Data.complete}}
+
+{{.pd.Data.team}}
+