WIP, beginning work on hash implementation.
This commit is contained in:
parent
ca2a85c81b
commit
a430bd97d1
7 changed files with 169 additions and 7 deletions
|
@ -77,12 +77,11 @@ but ShortForm imposes a configurable limit (10kb by default).
|
||||||
CREATE TABLE assets(
|
CREATE TABLE assets(
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
external_id UUID NOT NULL UNIQUE,
|
external_id UUID NOT NULL UNIQUE,
|
||||||
created_at TIMESTAMPTZ NOT NULL,
|
|
||||||
created_by BIGINT NOT NULL,
|
|
||||||
mime TEXT NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
extension TEXT NOT NULL,
|
extension TEXT NOT NULL,
|
||||||
hash TEXT NOT NULL
|
hash TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL,
|
||||||
|
created_by BIGINT NOT NULL
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package gs.shortform.crypto
|
||||||
|
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
|
||||||
|
object Sha256:
|
||||||
|
/**
|
||||||
|
* Calculate the SHA-256 hash for some data.
|
||||||
|
*
|
||||||
|
* @param data The data to hash.
|
||||||
|
* @return The base64-encoded [[Hash]] value.
|
||||||
|
*/
|
||||||
|
def calculateHash(data: Array[Byte]): Hash =
|
||||||
|
val digest: MessageDigest = MessageDigest.getInstance(Sha256.Algorithm)
|
||||||
|
Hash.encode(digest.digest(data))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consume the entire given stream and calculate its SHA-256 hash. This
|
||||||
|
* function _always_ closes the underlying stream.
|
||||||
|
*
|
||||||
|
* @param data The data to hash.
|
||||||
|
* @return The base64-encoded [[Hash]] value.
|
||||||
|
*/
|
||||||
|
def consumeToHash(data: BufferedInputStream): Hash =
|
||||||
|
try
|
||||||
|
// TODO: Need sum catz?
|
||||||
|
val digest: MessageDigest = MessageDigest.getInstance(Sha256.Algorithm)
|
||||||
|
val buffer: Array[Byte] = Array.ofDim[Byte](8192)
|
||||||
|
var count: Int = data.read(buffer)
|
||||||
|
while
|
||||||
|
count > 0
|
||||||
|
do
|
||||||
|
digest.update(buffer, 0, count)
|
||||||
|
count = data.read(buffer)
|
||||||
|
|
||||||
|
Hash.encode(digest.digest())
|
||||||
|
finally
|
||||||
|
data.close()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JCA Algorithm name for SHA-256.
|
||||||
|
*/
|
||||||
|
val Algorithm: String = "SHA-256"
|
||||||
|
|
||||||
|
end Sha256
|
33
modules/model/src/main/scala/gs/shortform/model/Asset.scala
Normal file
33
modules/model/src/main/scala/gs/shortform/model/Asset.scala
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package gs.shortform.model
|
||||||
|
|
||||||
|
import gs.uuid.UUID
|
||||||
|
import gs.shortform.crypto.Hash
|
||||||
|
|
||||||
|
case class Asset(
|
||||||
|
externalId: UUID,
|
||||||
|
title: Title,
|
||||||
|
extension: Asset.Extension,
|
||||||
|
hash: Hash,
|
||||||
|
createdAt: CreatedAt,
|
||||||
|
createdBy: Username
|
||||||
|
)
|
||||||
|
|
||||||
|
object Asset:
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a file extension for some [[Asset]].
|
||||||
|
*/
|
||||||
|
opaque type Extension = String
|
||||||
|
|
||||||
|
object Extension:
|
||||||
|
|
||||||
|
def apply(value: String): Extension = value
|
||||||
|
|
||||||
|
given CanEqual[Extension, Extension] = CanEqual.derived
|
||||||
|
|
||||||
|
extension (ext: Extension)
|
||||||
|
def str(): String = ext
|
||||||
|
|
||||||
|
end Extension
|
||||||
|
|
||||||
|
end Asset
|
|
@ -0,0 +1,25 @@
|
||||||
|
package gs.shortform.model
|
||||||
|
|
||||||
|
import gs.shortform.error.ShortFormError
|
||||||
|
|
||||||
|
sealed trait ModelError extends ShortFormError
|
||||||
|
|
||||||
|
object ModelError:
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Results from a [[Tag]] failing validation for being too short.
|
||||||
|
*
|
||||||
|
* @param candidate The candidate string.
|
||||||
|
* @param minimumLength The minimum required length.
|
||||||
|
*/
|
||||||
|
case class TagTooShort(candidate: String, minimumLength: Int) extends ModelError
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Results from a [[Tag]] failing validation for being too long.
|
||||||
|
*
|
||||||
|
* @param candidate The candidate string.
|
||||||
|
* @param maximumLength The maximum required length.
|
||||||
|
*/
|
||||||
|
case class TagTooLong(candidate: String, maximumLength: Int) extends ModelError
|
||||||
|
|
||||||
|
end ModelError
|
|
@ -15,11 +15,13 @@ import gs.shortform.crypto.Hash
|
||||||
* @param createdBy User who created this post.
|
* @param createdBy User who created this post.
|
||||||
* @param title Display title for this post.
|
* @param title Display title for this post.
|
||||||
* @param hash Hash of the primary rendered file for this post.
|
* @param hash Hash of the primary rendered file for this post.
|
||||||
|
* @param tags List of [[Tag]] applied to this post.
|
||||||
*/
|
*/
|
||||||
case class Post(
|
case class Post(
|
||||||
externalId: UUID,
|
externalId: UUID,
|
||||||
createdAt: CreatedAt,
|
createdAt: CreatedAt,
|
||||||
createdBy: Username,
|
createdBy: Username,
|
||||||
title: Title,
|
title: Title,
|
||||||
hash: Hash
|
hash: Hash,
|
||||||
|
tags: List[Tag]
|
||||||
)
|
)
|
||||||
|
|
57
modules/model/src/main/scala/gs/shortform/model/Tag.scala
Normal file
57
modules/model/src/main/scala/gs/shortform/model/Tag.scala
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package gs.shortform.model
|
||||||
|
|
||||||
|
import gs.shortform.error.ShortFormError
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arbitrary short string which can be used to annotate [[Post]]s.
|
||||||
|
*/
|
||||||
|
opaque type Tag = String
|
||||||
|
|
||||||
|
object Tag:
|
||||||
|
|
||||||
|
object Constraints:
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum allowed length for tags.
|
||||||
|
*/
|
||||||
|
val MinimumLength: Int = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum allowed length for tags.
|
||||||
|
*/
|
||||||
|
val MaximumLength: Int = 32
|
||||||
|
|
||||||
|
end Constraints
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate a new [[Tag]]. This function is unsafe.
|
||||||
|
*
|
||||||
|
* @param value The tag value.
|
||||||
|
* @return The new tag.
|
||||||
|
*/
|
||||||
|
def apply(value: String): Tag = value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate some candidate value to instantiate a new [[Tag]].
|
||||||
|
*
|
||||||
|
* @param value The candidate string value.
|
||||||
|
* @return The new tag, or an error if the input was invalid.
|
||||||
|
*/
|
||||||
|
def validate(value: String): Either[ShortFormError, Tag] =
|
||||||
|
if value.length() < Constraints.MinimumLength then
|
||||||
|
Left(ModelError.TagTooShort(value, Constraints.MinimumLength))
|
||||||
|
else if value.length() > Constraints.MaximumLength then
|
||||||
|
Left(ModelError.TagTooLong(value, Constraints.MaximumLength))
|
||||||
|
else
|
||||||
|
Right(value)
|
||||||
|
|
||||||
|
given CanEqual[Tag, Tag] = CanEqual.derived
|
||||||
|
|
||||||
|
extension (title: Tag)
|
||||||
|
/**
|
||||||
|
* Render this [[Tag]] as a string.
|
||||||
|
*/
|
||||||
|
def str(): String = title
|
||||||
|
|
||||||
|
end Tag
|
||||||
|
|
|
@ -12,8 +12,9 @@ object Username:
|
||||||
given CanEqual[Username, Username] = CanEqual.derived
|
given CanEqual[Username, Username] = CanEqual.derived
|
||||||
|
|
||||||
extension (title: Username)
|
extension (title: Username)
|
||||||
|
/**
|
||||||
|
* Render this [[Username]] as a string.
|
||||||
|
*/
|
||||||
def str(): String = title
|
def str(): String = title
|
||||||
|
|
||||||
end Username
|
end Username
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue