Example #1
0
File: core.go Project: z0rr0/luss
// tracker saves info customer short URL request.
func tracker(ch <-chan *CuInfo) {
	var wg sync.WaitGroup
	for cui := range ch {
		c, err := conf.FromContext(cui.ctx)
		if err != nil {
			logger.Printf("tracker wasn't called, error: %v", err)
			continue
		}
		if !c.Settings.TrackOn {
			c.L.Debug.Println("request tracking is disabled")
			continue
		}
		wg.Add(2)
		// tracker handler
		go func() {
			defer wg.Done()
			if err := stats.Tracker(cui.ctx, cui.cu, cui.addr); err != nil {
				c.L.Error.Println(err)
			}
		}()
		// callback handler
		go func() {
			defer wg.Done()
			// anonymous callbacks will not be handled
			if cui.cu.User != auth.Anonymous {
				if err := stats.Callback(cui.ctx, cui.cu); err != nil {
					c.L.Error.Println(err)
				}
			}
		}()
		wg.Wait()
	}
}
Example #2
0
File: core.go Project: z0rr0/luss
// HandlerIndex returns index web page.
func HandlerIndex(ctx context.Context, w http.ResponseWriter, r *http.Request) ErrHandler {
	data := map[string]string{}
	c, err := conf.FromContext(ctx)
	if err != nil {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	tpl, err := c.CacheTpl("base", "base.html", "index.html")
	if err != nil {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	if r.Method == "POST" {
		p, err := validateParams(r)
		if err != nil {
			c.L.Error.Println(err)
			data["Error"] = "Invalid data."
			err = tpl.ExecuteTemplate(w, "base", data)
			if err != nil {
				return ErrHandler{err, http.StatusInternalServerError}
			}
			return ErrHandler{nil, http.StatusOK}
		}
		params := []*trim.ReqParams{p}
		cus, err := trim.Shorten(ctx, params)
		if err != nil {
			return ErrHandler{err, http.StatusInternalServerError}
		}
		data["Result"] = c.Address(cus[0].String())
	}
	err = tpl.ExecuteTemplate(w, "base", data)
	if err != nil {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	return ErrHandler{nil, http.StatusOK}
}
Example #3
0
File: auth.go Project: z0rr0/luss
// CheckToken verifies the token, checks length and hash value.
// If the token is valid, then its 2nd part (hash) will be added to the returned context.
// It also marks empty token as ErrAnonymous error.
func CheckToken(ctx context.Context, r *http.Request, api bool) (context.Context, error) {
	var token string
	if api {
		if bearer := r.Header.Get("Authorization"); bearer != "" && !strings.HasPrefix(bearer, "Bearer") {
			return ctx, errors.New("invalid authorization data format")
		} else if bearer != "" {
			// split "Bearer"
			bearer = strings.Trim(bearer, ";")
			token = bearer[6:]
		}
	} else {
		token = r.PostFormValue("token")
	}
	l := len(token)
	if l == 0 {
		return setTokenContext(ctx, ""), ErrAnonymous
	}
	hexToken, err := hex.DecodeString(token)
	if err != nil {
		return ctx, err
	}
	c, err := conf.FromContext(ctx)
	if err != nil {
		return ctx, err
	}
	n := len(hexToken)
	if !EqualBytes(tokenHash(hexToken[:n/2], c), hexToken[n/2:]) {
		return ctx, errors.New("invalid token")
	}
	// hex.EncodeToString(h) == token[l/2:]
	return setTokenContext(ctx, token[l/2:]), nil
}
Example #4
0
File: core.go Project: z0rr0/luss
// HandlerNoWebIndex works like version but return only short link text.
func HandlerNoWebIndex(ctx context.Context, w http.ResponseWriter, r *http.Request) ErrHandler {
	c, err := conf.FromContext(ctx)
	if err != nil {
		fmt.Fprintf(w, "error: internal error\n")
		return ErrHandler{nil, http.StatusOK}
	}
	u := r.FormValue("u")
	if u == "" {
		fmt.Fprintf(w, "error: no data\n")
		return ErrHandler{nil, http.StatusOK}
	}
	param := &trim.ReqParams{
		Original:  u,
		Tag:       "",
		NotDirect: false,
		TTL:       nil,
	}
	err = param.Valid()
	if err != nil {
		fmt.Fprintf(w, "error: invalid url\n")
		return ErrHandler{nil, http.StatusOK}
	}
	params := []*trim.ReqParams{param}
	cus, err := trim.Shorten(ctx, params)
	if err != nil {
		fmt.Fprintf(w, "error: internal error\n")
		return ErrHandler{nil, http.StatusOK}
	}
	fmt.Fprintf(w, "%s\n", c.Address(cus[0].String()))
	return ErrHandler{nil, http.StatusOK}
}
Example #5
0
File: core.go Project: z0rr0/luss
// HandlerTest handles test GET request.
func HandlerTest(ctx context.Context, w http.ResponseWriter, r *http.Request) ErrHandler {
	c, err := conf.FromContext(ctx)
	if err != nil {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	coll, err := db.C(ctx, "tests")
	if err != nil {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	command := r.FormValue("write")
	switch {
	case c.Debug && command == "add":
		err = coll.Insert(bson.M{"ts": time.Now()})
	case c.Debug && command == "del":
		err = coll.Remove(nil)
	}
	if err != nil && err != mgo.ErrNotFound {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	n, err := coll.Count()
	if err != nil {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	u, err := auth.ExtractUser(ctx)
	if err != nil {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	c.L.Debug.Printf("user=%v", u)
	fmt.Fprintf(w, "found %v items", n)
	return ErrHandler{nil, http.StatusOK}
}
Example #6
0
File: core.go Project: z0rr0/luss
// HandlerRedirect searches saved original URL by a short one.
func HandlerRedirect(ctx context.Context, short string, r *http.Request) (string, error) {
	cu, err := trim.Lengthen(ctx, short)
	if err != nil {
		return "", err
	}
	c, err := conf.FromContext(ctx)
	if err != nil {
		return "", err
	}
	if c.Settings.TrackOn {
		ch, err := TrackerChan(ctx)
		if err != nil {
			// tracker's error is not critical
			// so only print it here
			c.L.Error.Println(err)
		} else {
			cui := &CuInfo{ctx, cu, r.RemoteAddr}
			if headProxy := c.Settings.TrackProxy; headProxy != "" {
				if proxyIP := r.Header.Get(headProxy); proxyIP != "" {
					_, port, _ := net.SplitHostPort(cui.addr)
					cui.addr = net.JoinHostPort(proxyIP, port)
				}
			}
			ch <- cui
		}

	}
	// TODO: check direct redirect
	return cu.Original, nil
}
Example #7
0
File: api.go Project: z0rr0/luss
// HandlerInfo returns main API info.
func HandlerInfo(ctx context.Context, w http.ResponseWriter, r *http.Request) core.ErrHandler {
	c, err := conf.FromContext(ctx)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	user, err := auth.ExtractUser(ctx)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	result := &infoResponse{
		Err: 0,
		Msg: "ok",
		Result: []infoResponseItem{
			infoResponseItem{
				Version:  Version,
				AuthOk:   !user.IsAnonymous(),
				PackSize: c.Settings.MaxPack,
			},
		},
	}
	b, err := json.Marshal(result)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, "%s", b)
	return core.ErrHandler{Err: nil, Status: http.StatusOK}
}
Example #8
0
File: trim.go Project: z0rr0/luss
// Lengthen converts a short link to original one.
// It uses own database session if it's needed
// or it gets data from the cache.
func Lengthen(ctx context.Context, short string) (*CustomURL, error) {
	c, err := conf.FromContext(ctx)
	if err != nil {
		return nil, err
	}
	cache, cacheOn := c.Cache.Strorage["URL"]
	if cacheOn {
		if cu, ok := cache.Get(short); ok {
			// c.L.Debug.Println("read from LRU cache", short)
			return cu.(*CustomURL), nil
		}
	}
	num, err := Decode(short)
	if err != nil {
		return nil, err
	}
	s, err := db.NewSession(c.Conn, false)
	if err != nil {
		return nil, err
	}
	defer s.Close()
	coll, err := db.Coll(s, "urls")
	if err != nil {
		return nil, err
	}
	cu := &CustomURL{}
	err = coll.Find(bson.D{{Name: "_id", Value: num}, {Name: "off", Value: false}}).One(cu)
	if err != nil {
		return nil, err
	}
	if cacheOn {
		cache.Add(short, cu)
	}
	return cu, nil
}
Example #9
0
File: trim.go Project: z0rr0/luss
// Shorten returns new short links.
func Shorten(ctx context.Context, params []*ReqParams) ([]*CustomURL, error) {
	c, err := conf.FromContext(ctx)
	if err != nil {
		return nil, err
	}
	u, err := auth.ExtractUser(ctx)
	if err != nil {
		return nil, err
	}
	// check URLs pack size
	n := len(params)
	if n > c.Settings.MaxPack {
		return nil, fmt.Errorf("too big ReqParams pack size [%v]", n)
	}
	s, err := db.CtxSession(ctx)
	if err != nil {
		return nil, err
	}
	// prepare
	coll, err := db.Coll(s, "urls")
	if err != nil {
		return nil, err
	}
	now := time.Now().UTC()
	err = db.LockURL(s)
	if err != nil {
		return nil, err
	}
	defer db.UnlockURL(s)
	num, err := getMax(coll)
	if err != nil {
		return nil, err
	}
	documents := make([]interface{}, n)
	cus := make([]*CustomURL, n)
	for i, param := range params {
		num++
		cus[i] = &CustomURL{
			ID:        num,
			Group:     param.Group,
			Tag:       param.Tag,
			Original:  param.Original,
			User:      u.Name,
			TTL:       param.TTL,
			NotDirect: param.NotDirect,
			Created:   now,
			Modified:  now,
			Cb:        param.Cb,
			API:       param.IsAPI,
		}
		documents[i] = cus[i]
	}
	err = coll.Insert(documents...)
	if err != nil {
		return nil, err
	}
	return cus, nil
}
Example #10
0
File: api.go Project: z0rr0/luss
// HandlerGet returns info about short URLs.
func HandlerGet(ctx context.Context, w http.ResponseWriter, r *http.Request) core.ErrHandler {
	var grs []getRequest
	c, err := conf.FromContext(ctx)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	defer r.Body.Close()
	decoder := json.NewDecoder(r.Body)
	err = decoder.Decode(&grs)
	if (err != nil) && (err != io.EOF) {
		return core.ErrHandler{Err: err, Status: http.StatusBadRequest}
	}
	links := []string{}
	for i := range grs {
		link, err := core.TrimAddress(grs[i].Short)
		if err != nil {
			c.L.Debug.Printf("invalid short URL [%v] was skipped: %v", link, err)
			continue
		}
		if l, ok := trim.IsShort(link); ok {
			links = append(links, l)
		}
	}
	if len(links) == 0 {
		return core.ErrHandler{Err: ErrEmptyRequest, Status: http.StatusNoContent}
	}
	cus, err := trim.MultiLengthen(ctx, links)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	items := make([]addResponseItem, len(cus))
	for i, cu := range cus {
		id := cu.Cu.String()
		items[i] = addResponseItem{
			ID:       id,
			Short:    c.Address(id),
			Original: cu.Cu.Original,
			Err:      cu.Err,
		}
	}
	result := &addResponse{
		Err:    0,
		Msg:    "ok",
		Result: items,
	}
	b, err := json.Marshal(result)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, "%s", b)
	return core.ErrHandler{Err: nil, Status: http.StatusOK}
}
Example #11
0
File: core.go Project: z0rr0/luss
// RunWorkers runs tracker workers.
func RunWorkers(ctx context.Context) (context.Context, error) {
	c, err := conf.FromContext(ctx)
	if err != nil {
		return ctx, err
	}
	ch := make(chan *CuInfo, trackerBuffer)
	for i := 0; i < c.Settings.Trackers; i++ {
		go func() {
			tracker(ch)
		}()
	}
	c.L.Info.Printf("run %v trackers", c.Settings.Trackers)
	return context.WithValue(ctx, trackerKey, ch), nil
}
Example #12
0
File: core.go Project: z0rr0/luss
// HandlerNotFound returns "not found" web page.
func HandlerNotFound(ctx context.Context, w http.ResponseWriter, r *http.Request) ErrHandler {
	c, err := conf.FromContext(ctx)
	if err != nil {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	tpl, err := c.CacheTpl("error", "base.html", "error.html")
	if err != nil {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	data := map[string]string{
		"Message": "The page is not found.",
		"Error":   "",
	}
	err = tpl.ExecuteTemplate(w, "base", data)
	if err != nil {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	return ErrHandler{nil, http.StatusOK}
}
Example #13
0
File: auth.go Project: z0rr0/luss
// ChangeUsers updates user's tokens.
// Administrator permissions should be checked before this call.
func ChangeUsers(ctx context.Context, names []string) ([]UserResult, error) {
	c, err := conf.FromContext(ctx)
	if err != nil {
		return nil, err
	}
	user, err := ExtractUser(ctx)
	if err != nil {
		return nil, err
	}
	s, err := db.CtxSession(ctx)
	if err != nil {
		return nil, err
	}
	coll, err := db.Coll(s, "users")
	if err != nil {
		return nil, err
	}
	now := time.Now().UTC()
	isAdmin := user.HasRole("admin")
	result := make([]UserResult, len(names))
	for i, name := range names {
		if !isAdmin && user.Name != name {
			result[i] = UserResult{Name: name, U: nil, Err: "permissions error"}
			continue
		}
		token, hash, err := genToken(c)
		if err != nil {
			result[i] = UserResult{Name: name, U: nil, Err: "internal error"}
			continue
		}
		err = coll.UpdateId(name, bson.M{"$set": bson.M{"token": hash, "mt": now}})
		if err != nil {
			errMsg := "internal error"
			if err == mgo.ErrNotFound {
				errMsg = "not found"
			}
			result[i] = UserResult{Name: name, U: nil, Err: errMsg}
			continue
		}
		result[i] = UserResult{Name: name, U: nil, T: token + hash, Err: ""}
	}
	return result, nil
}
Example #14
0
File: core.go Project: z0rr0/luss
// HandlerError returns "error" web page.
func HandlerError(ctx context.Context, w http.ResponseWriter, r *http.Request) ErrHandler {
	c, err := conf.FromContext(ctx)
	if err != nil {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	tpl, err := c.CacheTpl("error", "base.html", "error.html")
	if err != nil {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	data := map[string]string{
		"Message": "Error",
		"Error":   "The error occurred, probably due to internal server problems.",
	}
	err = tpl.ExecuteTemplate(w, "base", data)
	if err != nil {
		return ErrHandler{err, http.StatusInternalServerError}
	}
	return ErrHandler{nil, http.StatusOK}
}
Example #15
0
File: auth.go Project: z0rr0/luss
// CreateUsers creates new users.
func CreateUsers(ctx context.Context, names []string) ([]UserResult, error) {
	c, err := conf.FromContext(ctx)
	if err != nil {
		return nil, err
	}
	s, err := db.CtxSession(ctx)
	if err != nil {
		return nil, err
	}
	coll, err := db.Coll(s, "users")
	if err != nil {
		return nil, err
	}
	now := time.Now().UTC()
	result := make([]UserResult, len(names))
	for i, name := range names {
		token, hash, err := genToken(c)
		if err != nil {
			result[i] = UserResult{Name: name, U: nil, Err: "internal error"}
			continue
		}
		user := &User{
			Name:     name,
			Token:    hash,
			Roles:    []string{"user"},
			Modified: now,
			Created:  now,
		}
		if err := coll.Insert(user); err != nil {
			if mgo.IsDup(err) {
				result[i] = UserResult{Name: name, U: nil, Err: "duplicate item"}
			} else {
				result[i] = UserResult{Name: name, U: nil, Err: "internal error"}
			}
			continue
		}
		result[i] = UserResult{Name: name, U: user, T: token + hash, Err: ""}
	}
	return result, nil
}
Example #16
0
File: stats.go Project: z0rr0/luss
// Tracker saves info about short URL activities.
// GeoIP database can be loaded from
// http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz
func Tracker(ctx context.Context, cu *trim.CustomURL, addr string) error {
	c, err := conf.FromContext(ctx)
	if err != nil {
		return err
	}
	host, _, err := net.SplitHostPort(addr)
	if err != nil {
		return err
	}
	geo := GeoData{IP: host}
	record, err := c.GeoDB.City(net.ParseIP(host))
	if err != nil {
		// not critical: skip GeoIP data filling
		c.L.Error.Println(err)
	} else {
		geo.Country = record.Country.Names["en"]
		geo.City = record.City.Names["en"]
		geo.Latitude = record.Location.Latitude
		geo.Longitude = record.Location.Longitude
		geo.Tz = record.Location.TimeZone
	}
	s, err := db.NewSession(c.Conn, true)
	if err != nil {
		return err
	}
	defer s.Close()
	coll, err := db.Coll(s, "tracks")
	if err != nil {
		return err
	}
	err = coll.Insert(bson.M{
		"short": cu.String(),
		"url":   cu.Original,
		"group": cu.Group,
		"tag":   cu.Tag,
		"geo":   geo,
		"ts":    time.Now().UTC(),
	})
	return err
}
Example #17
0
File: trim.go Project: z0rr0/luss
// MultiLengthen returns short URLs info for slice of links.
func MultiLengthen(ctx context.Context, links []string) ([]ChangeResult, error) {
	var result []ChangeResult
	c, err := conf.FromContext(ctx)
	if err != nil {
		return nil, err
	}
	n := len(links)
	if n > c.Settings.MaxPack {
		return nil, fmt.Errorf("too big pack size [%v]", n)
	}
	s, err := db.CtxSession(ctx)
	if err != nil {
		return nil, err
	}
	coll, err := db.Coll(s, "urls")
	if err != nil {
		return nil, err
	}
	for _, link := range links {
		id, err := Decode(link)
		if err != nil {
			c.L.Error.Printf("decode error [%v]: %v", link, err)
			result = append(result, ChangeResult{Cu: &CustomURL{ID: id}, Err: "invalid value"})
			continue
		}
		cu := &CustomURL{}
		err = coll.FindId(id).One(cu)
		if err != nil {
			msg := "internal error"
			if err == mgo.ErrNotFound {
				msg = "not found"
			}
			result = append(result, ChangeResult{Cu: &CustomURL{ID: id}, Err: msg})
			continue
		}
		result = append(result, ChangeResult{Cu: cu})
	}
	return result, nil
}
Example #18
0
File: api.go Project: z0rr0/luss
// HandlerAdd creates new short URL.
func HandlerAdd(ctx context.Context, w http.ResponseWriter, r *http.Request) core.ErrHandler {
	c, err := conf.FromContext(ctx)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	defer r.Body.Close()
	params, err := validateAddParams(r)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusBadRequest}
	}
	cus, err := trim.Shorten(ctx, params)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	items := make([]addResponseItem, len(cus))
	for i, cu := range cus {
		id := cu.String()
		items[i] = addResponseItem{
			ID:       id,
			Short:    c.Address(id),
			Original: cu.Original,
		}
	}
	result := &addResponse{
		Err:    0,
		Msg:    "ok",
		Result: items,
	}
	b, err := json.Marshal(result)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, "%s", b)
	return core.ErrHandler{Err: nil, Status: http.StatusOK}
}
Example #19
0
File: api.go Project: z0rr0/luss
// HandlerExport exports URLs data.
func HandlerExport(ctx context.Context, w http.ResponseWriter, r *http.Request) core.ErrHandler {
	const (
		layout   = "2006-01-02"
		pageSize = 1000
	)
	c, err := conf.FromContext(ctx)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	user, err := auth.ExtractUser(ctx)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	if !user.HasRole("admin") {
		return core.ErrHandler{Err: errors.New("permissions error"), Status: http.StatusForbidden}
	}
	defer r.Body.Close()

	exp := &exportRequest{}
	decoder := json.NewDecoder(r.Body)
	err = decoder.Decode(exp)
	if (err != nil) && (err != io.EOF) {
		return core.ErrHandler{Err: err, Status: http.StatusBadRequest}
	}
	period, err := exp.parsePeriod()
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	filter := trim.Filter{
		Group:    exp.Group,
		Tag:      exp.Tag,
		Period:   period,
		Active:   exp.Active,
		Page:     exp.Page,
		PageSize: pageSize,
	}
	cus, pages, err := trim.Export(ctx, filter)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	items := make([]exportResponseItem, len(cus))
	for i, cu := range cus {
		id := cu.String()
		items[i] = exportResponseItem{
			ID:       id,
			Short:    c.Address(id),
			Original: cu.Original,
			Group:    cu.Group,
			Tag:      cu.Tag,
			Created:  cu.Created.UTC().Format(layout),
		}
	}
	result := &exportResponse{
		Err:    0,
		Msg:    "ok",
		Pages:  pages,
		Result: items,
	}
	b, err := json.Marshal(result)
	if err != nil {
		return core.ErrHandler{Err: err, Status: http.StatusInternalServerError}
	}
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, "%s", b)
	return core.ErrHandler{Err: nil, Status: http.StatusOK}
}
Example #20
0
File: trim.go Project: z0rr0/luss
// Import imports short URLs.
func Import(ctx context.Context, links map[string]*ReqParams) ([]ChangeResult, error) {
	var result []ChangeResult
	c, err := conf.FromContext(ctx)
	if err != nil {
		return nil, err
	}
	u, err := auth.ExtractUser(ctx)
	if err != nil {
		return nil, err
	}
	n := len(links)
	if n > c.Settings.MaxPack {
		return nil, fmt.Errorf("too big pack size [%v]", n)
	}
	s, err := db.CtxSession(ctx)
	if err != nil {
		return nil, err
	}
	// prepare
	coll, err := db.Coll(s, "urls")
	if err != nil {
		return nil, err
	}
	now := time.Now().UTC()
	for short, param := range links {
		num, err := Decode(short)
		if err != nil {
			result = append(result, ChangeResult{Err: "invalid short URL value"})
			continue
		}
		cu := &CustomURL{
			ID:        num,
			Group:     param.Group,
			Tag:       param.Tag,
			Original:  param.Original,
			User:      u.Name,
			TTL:       param.TTL,
			NotDirect: param.NotDirect,
			Created:   now,
			Modified:  now,
			Cb:        param.Cb,
			API:       param.IsAPI,
		}
		// a locking of every insert is not fast
		// but the pack doesn't lock other operations.
		err = db.LockURL(s)
		if err != nil {
			return nil, err
		}
		errIns := coll.Insert(cu)
		err = db.UnlockURL(s)
		if err != nil {
			return nil, err
		}
		if errIns != nil {
			msg := "internal error"
			if mgo.IsDup(errIns) {
				msg = "duplicate item"
			}
			result = append(result, ChangeResult{Err: msg})
			continue
		}
		result = append(result, ChangeResult{Cu: cu})
	}
	return result, nil
}