595 lines
17 KiB
Go
595 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"log"
|
|
"sync"
|
|
"errors"
|
|
"strings"
|
|
"strconv"
|
|
"database/sql"
|
|
|
|
"./query_gen/lib"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// TO-DO: Add the watchdog goroutine
|
|
var users UserStore
|
|
var err_account_exists = errors.New("This username is already in use.")
|
|
|
|
type UserStore interface {
|
|
Load(id int) error
|
|
Get(id int) (*User, error)
|
|
GetUnsafe(id int) (*User, error)
|
|
CascadeGet(id int) (*User, error)
|
|
//BulkCascadeGet(ids []int) ([]*User, error)
|
|
BulkCascadeGetMap(ids []int) (map[int]*User, error)
|
|
BypassGet(id int) (*User, error)
|
|
Set(item *User) error
|
|
Add(item *User) error
|
|
AddUnsafe(item *User) error
|
|
Remove(id int) error
|
|
RemoveUnsafe(id int) error
|
|
CreateUser(username string, password string, email string, group int, active int) (int, error)
|
|
GetLength() int
|
|
GetCapacity() int
|
|
GetGlobalCount() int
|
|
}
|
|
|
|
type MemoryUserStore struct {
|
|
items map[int]*User
|
|
length int
|
|
capacity int
|
|
get *sql.Stmt
|
|
register *sql.Stmt
|
|
username_exists *sql.Stmt
|
|
user_count *sql.Stmt
|
|
sync.RWMutex
|
|
}
|
|
|
|
func NewMemoryUserStore(capacity int) *MemoryUserStore {
|
|
get_stmt, err := qgen.Builder.SimpleSelect("users","name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group","uid = ?","","")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Add an admin version of register_stmt with more flexibility?
|
|
// create_account_stmt, err = db.Prepare("INSERT INTO
|
|
register_stmt, err := qgen.Builder.SimpleInsert("users","name, email, password, salt, group, is_super_admin, session, active, message","?,?,?,?,?,0,'',?,''")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
username_exists_stmt, err := qgen.Builder.SimpleSelect("users","name","name = ?","","")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
user_count_stmt, err := qgen.Builder.SimpleCount("users","","")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
return &MemoryUserStore{
|
|
items: make(map[int]*User),
|
|
capacity: capacity,
|
|
get: get_stmt,
|
|
register: register_stmt,
|
|
username_exists: username_exists_stmt,
|
|
user_count: user_count_stmt,
|
|
}
|
|
}
|
|
|
|
func (sus *MemoryUserStore) Get(id int) (*User, error) {
|
|
sus.RLock()
|
|
item, ok := sus.items[id]
|
|
sus.RUnlock()
|
|
if ok {
|
|
return item, nil
|
|
}
|
|
return item, ErrNoRows
|
|
}
|
|
|
|
func (sus *MemoryUserStore) GetUnsafe(id int) (*User, error) {
|
|
item, ok := sus.items[id]
|
|
if ok {
|
|
return item, nil
|
|
}
|
|
return item, ErrNoRows
|
|
}
|
|
|
|
func (sus *MemoryUserStore) CascadeGet(id int) (*User, error) {
|
|
sus.RLock()
|
|
user, ok := sus.items[id]
|
|
sus.RUnlock()
|
|
if ok {
|
|
return user, nil
|
|
}
|
|
|
|
user = &User{ID:id,Loggedin:true}
|
|
err := sus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Is_Super_Admin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Last_IP, &user.TempGroup)
|
|
|
|
if user.Avatar != "" {
|
|
if user.Avatar[0] == '.' {
|
|
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
|
|
}
|
|
} else {
|
|
user.Avatar = strings.Replace(config.Noavatar,"{id}",strconv.Itoa(user.ID),1)
|
|
}
|
|
user.Link = build_profile_url(name_to_slug(user.Name),id)
|
|
user.Tag = groups[user.Group].Tag
|
|
init_user_perms(user)
|
|
if err == nil {
|
|
sus.Set(user)
|
|
}
|
|
return user, err
|
|
}
|
|
|
|
// WARNING: We did a little hack to make this as thin and quick as possible to reduce lock contention, use the * Cascade* methods instead for normal use
|
|
func (sus *MemoryUserStore) bulkGet(ids []int) (list []*User) {
|
|
list = make([]*User,len(ids))
|
|
sus.RLock()
|
|
for i, id := range ids {
|
|
list[i] = sus.items[id]
|
|
}
|
|
sus.RUnlock()
|
|
return list
|
|
}
|
|
|
|
// TO-DO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts?
|
|
// TO-DO: ID of 0 should always error?
|
|
func (sus *MemoryUserStore) BulkCascadeGetMap(ids []int) (list map[int]*User, err error) {
|
|
var id_count int = len(ids)
|
|
list = make(map[int]*User)
|
|
if id_count == 0 {
|
|
return list, nil
|
|
}
|
|
|
|
var still_here []int
|
|
slice_list := sus.bulkGet(ids)
|
|
for i, slice_item := range slice_list {
|
|
if slice_item != nil {
|
|
list[slice_item.ID] = slice_item
|
|
} else {
|
|
still_here = append(still_here,ids[i])
|
|
}
|
|
}
|
|
ids = still_here
|
|
|
|
// If every user is in the cache, then return immediately
|
|
if len(ids) == 0 {
|
|
return list, nil
|
|
}
|
|
|
|
var qlist string
|
|
var uidList []interface{}
|
|
for _, id := range ids {
|
|
uidList = append(uidList,strconv.Itoa(id))
|
|
qlist += "?,"
|
|
}
|
|
qlist = qlist[0:len(qlist) - 1]
|
|
|
|
stmt, err := qgen.Builder.SimpleSelect("users","uid, name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group","uid IN("+qlist+")","","")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rows, err := stmt.Query(uidList...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for rows.Next() {
|
|
user := &User{Loggedin:true}
|
|
err := rows.Scan(&user.ID, &user.Name, &user.Group, &user.Is_Super_Admin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Last_IP, &user.TempGroup)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Initialise the user
|
|
if user.Avatar != "" {
|
|
if user.Avatar[0] == '.' {
|
|
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
|
|
}
|
|
} else {
|
|
user.Avatar = strings.Replace(config.Noavatar,"{id}",strconv.Itoa(user.ID),1)
|
|
}
|
|
user.Link = build_profile_url(name_to_slug(user.Name),user.ID)
|
|
user.Tag = groups[user.Group].Tag
|
|
init_user_perms(user)
|
|
|
|
// Add it to the cache...
|
|
sus.Set(user)
|
|
|
|
// Add it to the list to be returned
|
|
list[user.ID] = user
|
|
}
|
|
|
|
// Did we miss any users?
|
|
if id_count > len(list) {
|
|
var sid_list string
|
|
for _, id := range ids {
|
|
_, ok := list[id]
|
|
if !ok {
|
|
sid_list += strconv.Itoa(id) + ","
|
|
}
|
|
}
|
|
|
|
// We probably don't need this, but it might be useful in case of bugs in BulkCascadeGetMap
|
|
if sid_list == "" {
|
|
if dev.DebugMode {
|
|
log.Print("This data is sampled later in the BulkCascadeGetMap function, so it might miss the cached IDs")
|
|
log.Print("id_count",id_count)
|
|
log.Print("ids",ids)
|
|
log.Print("list",list)
|
|
}
|
|
return list, errors.New("We weren't able to find a user, but we don't know which one")
|
|
}
|
|
sid_list = sid_list[0:len(sid_list) - 1]
|
|
|
|
return list, errors.New("Unable to find the users with the following IDs: " + sid_list)
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (sus *MemoryUserStore) BypassGet(id int) (*User, error) {
|
|
user := &User{ID:id,Loggedin:true}
|
|
err := sus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Is_Super_Admin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Last_IP, &user.TempGroup)
|
|
|
|
if user.Avatar != "" {
|
|
if user.Avatar[0] == '.' {
|
|
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
|
|
}
|
|
} else {
|
|
user.Avatar = strings.Replace(config.Noavatar,"{id}",strconv.Itoa(user.ID),1)
|
|
}
|
|
user.Link = build_profile_url(name_to_slug(user.Name),id)
|
|
user.Tag = groups[user.Group].Tag
|
|
init_user_perms(user)
|
|
return user, err
|
|
}
|
|
|
|
func (sus *MemoryUserStore) Load(id int) error {
|
|
user := &User{ID:id,Loggedin:true}
|
|
err := sus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Is_Super_Admin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Last_IP, &user.TempGroup)
|
|
if err != nil {
|
|
sus.Remove(id)
|
|
return err
|
|
}
|
|
|
|
if user.Avatar != "" {
|
|
if user.Avatar[0] == '.' {
|
|
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
|
|
}
|
|
} else {
|
|
user.Avatar = strings.Replace(config.Noavatar,"{id}",strconv.Itoa(user.ID),1)
|
|
}
|
|
user.Link = build_profile_url(name_to_slug(user.Name),id)
|
|
user.Tag = groups[user.Group].Tag
|
|
init_user_perms(user)
|
|
sus.Set(user)
|
|
return nil
|
|
}
|
|
|
|
func (sus *MemoryUserStore) Set(item *User) error {
|
|
sus.Lock()
|
|
user, ok := sus.items[item.ID]
|
|
if ok {
|
|
sus.Unlock()
|
|
*user = *item
|
|
} else if sus.length >= sus.capacity {
|
|
sus.Unlock()
|
|
return ErrStoreCapacityOverflow
|
|
} else {
|
|
sus.items[item.ID] = item
|
|
sus.Unlock()
|
|
sus.length++
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (sus *MemoryUserStore) Add(item *User) error {
|
|
if sus.length >= sus.capacity {
|
|
return ErrStoreCapacityOverflow
|
|
}
|
|
sus.Lock()
|
|
sus.items[item.ID] = item
|
|
sus.Unlock()
|
|
sus.length++
|
|
return nil
|
|
}
|
|
|
|
func (sus *MemoryUserStore) AddUnsafe(item *User) error {
|
|
if sus.length >= sus.capacity {
|
|
return ErrStoreCapacityOverflow
|
|
}
|
|
sus.items[item.ID] = item
|
|
sus.length++
|
|
return nil
|
|
}
|
|
|
|
func (sus *MemoryUserStore) Remove(id int) error {
|
|
sus.Lock()
|
|
delete(sus.items,id)
|
|
sus.Unlock()
|
|
sus.length--
|
|
return nil
|
|
}
|
|
|
|
func (sus *MemoryUserStore) RemoveUnsafe(id int) error {
|
|
delete(sus.items,id)
|
|
sus.length--
|
|
return nil
|
|
}
|
|
|
|
func (sus *MemoryUserStore) CreateUser(username string, password string, email string, group int, active int) (int, error) {
|
|
// Is this username already taken..?
|
|
err := sus.username_exists.QueryRow(username).Scan(&username)
|
|
if err != ErrNoRows {
|
|
return 0, err_account_exists
|
|
}
|
|
|
|
salt, err := GenerateSafeString(saltLength)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
hashed_password, err := bcrypt.GenerateFromPassword([]byte(password + salt), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
res, err := sus.register.Exec(username,email,string(hashed_password),salt,group,active)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
lastId, err := res.LastInsertId()
|
|
return int(lastId), err
|
|
}
|
|
|
|
func (sus *MemoryUserStore) GetLength() int {
|
|
return sus.length
|
|
}
|
|
|
|
func (sus *MemoryUserStore) SetCapacity(capacity int) {
|
|
sus.capacity = capacity
|
|
}
|
|
|
|
func (sus *MemoryUserStore) GetCapacity() int {
|
|
return sus.capacity
|
|
}
|
|
|
|
// Return the total number of users registered on the forums
|
|
func (sus *MemoryUserStore) GetGlobalCount() int {
|
|
var ucount int
|
|
err := sus.user_count.QueryRow().Scan(&ucount)
|
|
if err != nil {
|
|
LogError(err)
|
|
}
|
|
return ucount
|
|
}
|
|
|
|
type SqlUserStore struct {
|
|
get *sql.Stmt
|
|
register *sql.Stmt
|
|
username_exists *sql.Stmt
|
|
user_count *sql.Stmt
|
|
}
|
|
|
|
func NewSqlUserStore() *SqlUserStore {
|
|
get_stmt, err := qgen.Builder.SimpleSelect("users","name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group","uid = ?","","")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Add an admin version of register_stmt with more flexibility?
|
|
// create_account_stmt, err = db.Prepare("INSERT INTO
|
|
register_stmt, err := qgen.Builder.SimpleInsert("users","name, email, password, salt, group, is_super_admin, session, active, message","?,?,?,?,?,0,'',?,''")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
username_exists_stmt, err := qgen.Builder.SimpleSelect("users","name","name = ?","","")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
user_count_stmt, err := qgen.Builder.SimpleCount("users","","")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
return &SqlUserStore{
|
|
get: get_stmt,
|
|
register: register_stmt,
|
|
username_exists: username_exists_stmt,
|
|
user_count: user_count_stmt,
|
|
}
|
|
}
|
|
|
|
func (sus *SqlUserStore) Get(id int) (*User, error) {
|
|
user := User{ID:id,Loggedin:true}
|
|
err := sus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Is_Super_Admin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Last_IP, &user.TempGroup)
|
|
|
|
if user.Avatar != "" {
|
|
if user.Avatar[0] == '.' {
|
|
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
|
|
}
|
|
} else {
|
|
user.Avatar = strings.Replace(config.Noavatar,"{id}",strconv.Itoa(user.ID),1)
|
|
}
|
|
user.Link = build_profile_url(name_to_slug(user.Name),id)
|
|
user.Tag = groups[user.Group].Tag
|
|
init_user_perms(&user)
|
|
return &user, err
|
|
}
|
|
|
|
func (sus *SqlUserStore) GetUnsafe(id int) (*User, error) {
|
|
user := User{ID:id,Loggedin:true}
|
|
err := sus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Is_Super_Admin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Last_IP, &user.TempGroup)
|
|
|
|
if user.Avatar != "" {
|
|
if user.Avatar[0] == '.' {
|
|
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
|
|
}
|
|
} else {
|
|
user.Avatar = strings.Replace(config.Noavatar,"{id}",strconv.Itoa(user.ID),1)
|
|
}
|
|
user.Link = build_profile_url(name_to_slug(user.Name),id)
|
|
user.Tag = groups[user.Group].Tag
|
|
init_user_perms(&user)
|
|
return &user, err
|
|
}
|
|
|
|
func (sus *SqlUserStore) CascadeGet(id int) (*User, error) {
|
|
user := User{ID:id,Loggedin:true}
|
|
err := sus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Is_Super_Admin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Last_IP, &user.TempGroup)
|
|
|
|
if user.Avatar != "" {
|
|
if user.Avatar[0] == '.' {
|
|
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
|
|
}
|
|
} else {
|
|
user.Avatar = strings.Replace(config.Noavatar,"{id}",strconv.Itoa(user.ID),1)
|
|
}
|
|
user.Link = build_profile_url(name_to_slug(user.Name),id)
|
|
user.Tag = groups[user.Group].Tag
|
|
init_user_perms(&user)
|
|
return &user, err
|
|
}
|
|
|
|
// TO-DO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts?
|
|
func (sus *SqlUserStore) BulkCascadeGetMap(ids []int) (list map[int]*User, err error) {
|
|
var qlist string
|
|
var uidList []interface{}
|
|
for _, id := range ids {
|
|
uidList = append(uidList,strconv.Itoa(id))
|
|
qlist += "?,"
|
|
}
|
|
qlist = qlist[0:len(qlist) - 1]
|
|
|
|
stmt, err := qgen.Builder.SimpleSelect("users","uid, name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group","uid IN("+qlist+")","","")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rows, err := stmt.Query(uidList...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
list = make(map[int]*User)
|
|
for rows.Next() {
|
|
user := &User{Loggedin:true}
|
|
err := rows.Scan(&user.ID, &user.Name, &user.Group, &user.Is_Super_Admin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Last_IP, &user.TempGroup)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Initialise the user
|
|
if user.Avatar != "" {
|
|
if user.Avatar[0] == '.' {
|
|
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
|
|
}
|
|
} else {
|
|
user.Avatar = strings.Replace(config.Noavatar,"{id}",strconv.Itoa(user.ID),1)
|
|
}
|
|
user.Link = build_profile_url(name_to_slug(user.Name),user.ID)
|
|
user.Tag = groups[user.Group].Tag
|
|
init_user_perms(user)
|
|
|
|
// Add it to the list to be returned
|
|
list[user.ID] = user
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (sus *SqlUserStore) BypassGet(id int) (*User, error) {
|
|
user := User{ID:id,Loggedin:true}
|
|
err := sus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Is_Super_Admin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Last_IP, &user.TempGroup)
|
|
|
|
if user.Avatar != "" {
|
|
if user.Avatar[0] == '.' {
|
|
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
|
|
}
|
|
} else {
|
|
user.Avatar = strings.Replace(config.Noavatar,"{id}",strconv.Itoa(user.ID),1)
|
|
}
|
|
user.Link = build_profile_url(name_to_slug(user.Name),id)
|
|
user.Tag = groups[user.Group].Tag
|
|
init_user_perms(&user)
|
|
return &user, err
|
|
}
|
|
|
|
func (sus *SqlUserStore) Load(id int) error {
|
|
user := &User{ID:id}
|
|
// Simplify this into a quick check to see whether the user exists. Add an Exists method to facilitate this?
|
|
return sus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Is_Super_Admin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Last_IP, &user.TempGroup)
|
|
}
|
|
|
|
func (sus *SqlUserStore) CreateUser(username string, password string, email string, group int, active int) (int, error) {
|
|
// Is this username already taken..?
|
|
err := sus.username_exists.QueryRow(username).Scan(&username)
|
|
if err != ErrNoRows {
|
|
return 0, err_account_exists
|
|
}
|
|
|
|
salt, err := GenerateSafeString(saltLength)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
hashed_password, err := bcrypt.GenerateFromPassword([]byte(password + salt), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
res, err := sus.register.Exec(username,email,string(hashed_password),salt,group,active)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
lastId, err := res.LastInsertId()
|
|
return int(lastId), err
|
|
}
|
|
|
|
// Placeholder methods, as we're not don't need to do any cache management with this implementation ofr the UserStore
|
|
func (sus *SqlUserStore) Set(item *User) error {
|
|
return nil
|
|
}
|
|
func (sus *SqlUserStore) Add(item *User) error {
|
|
return nil
|
|
}
|
|
func (sus *SqlUserStore) AddUnsafe(item *User) error {
|
|
return nil
|
|
}
|
|
func (sus *SqlUserStore) Remove(id int) error {
|
|
return nil
|
|
}
|
|
func (sus *SqlUserStore) RemoveUnsafe(id int) error {
|
|
return nil
|
|
}
|
|
func (sus *SqlUserStore) GetCapacity() int {
|
|
return 0
|
|
}
|
|
|
|
// Return the total number of users registered on the forums
|
|
func (sus *SqlUserStore) GetLength() int {
|
|
var ucount int
|
|
err := sus.user_count.QueryRow().Scan(&ucount)
|
|
if err != nil {
|
|
LogError(err)
|
|
}
|
|
return ucount
|
|
}
|
|
func (sus *SqlUserStore) GetGlobalCount() int {
|
|
var ucount int
|
|
err := sus.user_count.QueryRow().Scan(&ucount)
|
|
if err != nil {
|
|
LogError(err)
|
|
}
|
|
return ucount
|
|
}
|