-
Notifications
You must be signed in to change notification settings - Fork 0
/
shorturl.go
208 lines (173 loc) · 5.85 KB
/
shorturl.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
// Yordle - A URL shortener for Google App Engine.
// Copyright (C) 2014 The Yordle Team
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
// Package shorturl provides functionality for the basic CRUD operations.
package shorturl
import (
"context"
"crypto/md5"
"crypto/sha1"
"crypto/sha512"
"errors"
"fmt"
"log"
"cloud.google.com/go/datastore"
"github.com/qqiao/yordle/config"
)
const (
// KindName is the datastore kind name for short url.
KindName = "ShortUrl"
)
// ShortURL is the entity used for storing short URL. Because of the fact that
// URLs can be more than 500 characters long and that we cannot index string
// properties longer than 500 characters directly to ensure uniqueness, we have
// to use the hash code of the URL as a fallback.
//
// We also use multiple hash algorithms just to eliminate the possibility of
// hash collisions of one particular hashing algorithm. Using SHA512, SHA1, and
// MD5 simultaneously should make a hash collision statistically extremely.
// improbable.
type ShortURL struct {
ID int64 `datastore:"-"`
Hash string
OriginalURL string `datastore:"OriginalUrl,noindex"`
}
// UniqueKey is an entity whose sole purpose is to ensure the
// uniqueness of a ShortURL.
type UniqueKey struct {
ID int64
}
// Error values
var (
// ErrDatastoreInconsistent is returned when the datastore is in an
// Inconsistent state. Typical example would be when the memcache
// indicates that there should've been an instance of the short URL
// stored but the actual instance cannot be found.
ErrDatastoreInconsistent = errors.New("Datastore Inconsistent")
ErrDatastoreWriteDisabled = errors.New("Datastore write disabled")
// ErrNotFound is the error to be raised when the short URL matching the
// search criteria cannot be found
ErrNotFound = errors.New("Short URL not found")
)
// ByID loads the short URL by its ID.
func ByID(ctx context.Context, id int64) (*ShortURL, error) {
client, err := datastore.NewClient(ctx, config.ProjectName)
if nil != err {
log.Printf("Unable to create datastore client. Error: %s", err)
return nil, err
}
var shortURL ShortURL
if err := client.Get(ctx,
datastore.IDKey(KindName, id, nil),
&shortURL); nil != err {
return nil, err
}
shortURL.ID = id
return &shortURL, nil
}
// ByURL finds the short URL by its original long URL.
func ByURL(ctx context.Context, url string) (*ShortURL, error) {
hash := hash(url)
client, err := datastore.NewClient(ctx, config.ProjectName)
if nil != err {
log.Printf("Unable to create datastore client. Error: %s", err)
return nil, err
}
var results []*ShortURL
q := datastore.NewQuery(KindName).
// Ancestor(uniqueKey).
Filter("Hash = ", hash).
Limit(1)
keys, err := client.GetAll(ctx, q, &results)
if nil != err {
return nil, err
}
if len(results) < 1 {
return nil, ErrNotFound
}
shortURL := results[0]
shortURL.ID = keys[0].ID
return shortURL, nil
}
// List lists all short URLs shorted by the their IDs.
func List(ctx context.Context, start int64, count int) ([]ShortURL, error) {
return nil, nil
}
func hash(originalURL string) string {
bytes := []byte(originalURL)
return fmt.Sprintf("%x|%x|%x", sha512.Sum512(bytes), sha1.Sum(bytes), md5.Sum(bytes))
}
func keys(originalURL string) (*datastore.Key, *datastore.Key, string) {
hash := hash(originalURL)
uniqueKey := datastore.NameKey("Unique", originalURL, nil)
objectKey := datastore.IncompleteKey(KindName, nil)
return objectKey, uniqueKey, hash
}
// Persist persists the long URL by creating necessary objects.
func Persist(ctx context.Context, originalURL string) (*ShortURL, error) {
var client *datastore.Client
var err error
if client, err = datastore.NewClient(ctx, config.ProjectName); nil != err {
log.Printf("Unable to create datastore client. Error: %s", err)
return nil, err
}
objectKey, uniqueKey, hash := keys(originalURL)
var sk *datastore.PendingKey
shortURL := &ShortURL{}
var commit *datastore.Commit
if commit, err = client.RunInTransaction(ctx,
func(tx *datastore.Transaction) error {
log.Printf("Persisting ShortURL for '%s'...", originalURL)
var uk UniqueKey
err = tx.Get(uniqueKey, &uk)
if nil == err {
log.Printf("ShortURL for '%s' already exists, loading...",
originalURL)
shortURL, err = ByURL(ctx, originalURL)
log.Printf("ShortURL loaded: %v", shortURL)
return err
}
if nil != err {
if datastore.ErrNoSuchEntity != err {
log.Printf("Error checking uniqueness: %v", err)
return err
}
log.Println("No unique key found, proceeding with persistence")
}
log.Printf("Storing actual ShortURL Object for '%s'", originalURL)
shortURL.Hash = hash
shortURL.OriginalURL = originalURL
var err error
if sk, err = tx.Put(objectKey, shortURL); nil != err {
log.Printf("Error storing ShortURL Object: %v", err)
return err
}
log.Printf("Done storing ShortURL for '%s'.", originalURL)
log.Printf("Storing UniqueKey for '%s'.", originalURL)
if _, err = tx.Put(uniqueKey, &UniqueKey{}); nil != err {
log.Printf("Error storing ShortURL Unique Key: %v", err)
return err
}
return nil
}); nil != err {
return nil, err
}
if nil != sk {
key := commit.Key(sk)
shortURL.ID = key.ID
}
return shortURL, nil
}