artserve 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Manifest.txt +1 -0
- data/README.md +31 -2
- data/lib/artserve/public/artbase.js +160 -0
- data/lib/artserve/public/style.css +129 -10
- data/lib/artserve/service.rb +12 -0
- data/lib/artserve/version.rb +2 -2
- data/lib/artserve/views/index.erb +72 -3
- data/lib/artserve/views/layout.erb +2 -1
- data/lib/artserve.rb +16 -3
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60a1c24aca28429732b6b6b90a2008b3abd8d56e5e7849175c7a16701b7dbdbf
|
4
|
+
data.tar.gz: f8c745e129b3d4b421f5681056b5dd6d8507244ef69c5f40cdee56a0e4ae4b05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a48553cf45dff026e510acfa29aaa7af1d10623807233ca10a2bc1b5cb3b0ff9320c30194b0efce64d20df7ec27cb39ec6da9e08c2277b8e0b471ec9b619ab7d
|
7
|
+
data.tar.gz: 6bc157bfdab797976939144dde837de49aa98d9b3dfb9c0ad180f33e1ae44986981e184715bb8841c1f4ea361b4904f5ac2208346d963eff8fefe1f5a7f3402b
|
data/Manifest.txt
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# Artserve
|
2
2
|
|
3
3
|
|
4
|
-
artserve - serve up single-file SQLite artbase dbs to query metadata and images with SQL and more
|
4
|
+
artserve - serve up single-file SQLite artbase dbs to query metadata and images with SQL and more
|
5
5
|
|
6
6
|
|
7
7
|
* home :: [github.com/pixelartexchange/artbase](https://github.com/pixelartexchange/artbase)
|
@@ -13,7 +13,36 @@ artserve - serve up single-file SQLite artbase dbs to query metadata and images
|
|
13
13
|
|
14
14
|
## Command-Line Tool
|
15
15
|
|
16
|
-
|
16
|
+
Use the command line tool named - surprise, surpirse - `artserve`
|
17
|
+
to run a zero-config / out-of-the-box artbase server that lets
|
18
|
+
you query entire collections in single sqlite database (metadata & images) with a "serverless" web page.
|
19
|
+
Type:
|
20
|
+
|
21
|
+
$ artserve # defaults to ./artbase.db
|
22
|
+
|
23
|
+
That will start-up a (local loopback) web server running on port 3000.
|
24
|
+
Open-up up the index page in your browser to get started e.g. <http://localhost:3000/>.
|
25
|
+
|
26
|
+
That's it.
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
**`artbase.db` Options**
|
31
|
+
|
32
|
+
If you pass in a directory to artserve
|
33
|
+
the machinery will look for an `artbase.db` in the directory e.g.
|
34
|
+
|
35
|
+
$ artserve moonbirds # defaults to ./moonbirds/artbase.db
|
36
|
+
$ artserve goblintown # defaults to ./goblintown/artbase.db
|
37
|
+
# ...
|
38
|
+
|
39
|
+
If you pass in a file to artserve
|
40
|
+
the machinery will use the bespoke name & path to look for the sqlite database e.g.
|
41
|
+
|
42
|
+
$ artserve punkbase.db
|
43
|
+
$ artserve moonbirdbase.db
|
44
|
+
# ...
|
45
|
+
|
17
46
|
|
18
47
|
|
19
48
|
|
@@ -0,0 +1,160 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
// Load a script from given `url`
|
4
|
+
function loadScript(url) {
|
5
|
+
return new Promise(function (resolve, reject) {
|
6
|
+
const script = document.createElement('script');
|
7
|
+
script.src = url;
|
8
|
+
|
9
|
+
script.addEventListener('load', function () {
|
10
|
+
// The script is loaded completely
|
11
|
+
resolve(true);
|
12
|
+
});
|
13
|
+
|
14
|
+
document.head.appendChild(script);
|
15
|
+
});
|
16
|
+
}
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
class Artbase {
|
22
|
+
|
23
|
+
|
24
|
+
async init( options={} ) {
|
25
|
+
|
26
|
+
const DEFAULTS = {
|
27
|
+
database: "artbase.db",
|
28
|
+
};
|
29
|
+
|
30
|
+
this.settings = Object.assign( {}, DEFAULTS, options );
|
31
|
+
|
32
|
+
console.log( "options:" );
|
33
|
+
console.log( options );
|
34
|
+
console.log( "settings:" );
|
35
|
+
console.log( this.settings );
|
36
|
+
|
37
|
+
|
38
|
+
// todo/fix:
|
39
|
+
// bundle "offline" with artserve - why? why not?
|
40
|
+
|
41
|
+
console.log( "fetching sql.js..." );
|
42
|
+
await loadScript( 'https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.6.1/sql-wasm.js' );
|
43
|
+
console.log( "done fetching sql.js" );
|
44
|
+
|
45
|
+
|
46
|
+
const sqlPromise = initSqlJs({
|
47
|
+
locateFile: file => "https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.6.1/sql-wasm.wasm"
|
48
|
+
});
|
49
|
+
|
50
|
+
|
51
|
+
const dataPromise = fetch( this.settings.database ).then(res => res.arrayBuffer());
|
52
|
+
const [SQL, buf] = await Promise.all([sqlPromise, dataPromise])
|
53
|
+
this.db = new SQL.Database(new Uint8Array(buf));
|
54
|
+
}
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
_build_query() {
|
59
|
+
// get select query as string
|
60
|
+
let select = document.querySelector("#select").value
|
61
|
+
if (select.length === 0) {
|
62
|
+
select = "*"
|
63
|
+
} else {
|
64
|
+
if (select !== "*") {
|
65
|
+
if (!/.*image.*/.test(select)) {
|
66
|
+
select = select + ", image"
|
67
|
+
}
|
68
|
+
if (!/.*id.*/.test(select)) {
|
69
|
+
select = select + ", id"
|
70
|
+
}
|
71
|
+
}
|
72
|
+
}
|
73
|
+
let where = document.querySelector("#where").value
|
74
|
+
let limit = parseInt(document.querySelector("#limit").value)
|
75
|
+
|
76
|
+
let sql = `SELECT ${select} FROM metadata`
|
77
|
+
if (where.length > 0) {
|
78
|
+
sql += ` WHERE ${where}`
|
79
|
+
}
|
80
|
+
if (limit > 0) {
|
81
|
+
sql += ` LIMIT ${limit}`
|
82
|
+
} else {
|
83
|
+
sql += " LIMIT 200"
|
84
|
+
}
|
85
|
+
|
86
|
+
return sql
|
87
|
+
}
|
88
|
+
|
89
|
+
|
90
|
+
_build_records( result ) {
|
91
|
+
let records = []
|
92
|
+
|
93
|
+
if( result.length !== 0) {
|
94
|
+
let columns = []
|
95
|
+
|
96
|
+
for(let column of result[0].columns) {
|
97
|
+
columns.push(column)
|
98
|
+
}
|
99
|
+
|
100
|
+
for(let i=0; i<result[0].values.length; i++) {
|
101
|
+
let values = result[0].values[i];
|
102
|
+
let o = { attributes: {} }
|
103
|
+
for(let j=0; j<columns.length; j++) {
|
104
|
+
let column = columns[j]
|
105
|
+
// add id to "hidden" system properties / attributes - why? why not?
|
106
|
+
if (["id",
|
107
|
+
"image",
|
108
|
+
"created_at",
|
109
|
+
"updated_at" ].includes(column)) {
|
110
|
+
o[column] = values[j]
|
111
|
+
} else {
|
112
|
+
// only add non-null attributes - why? why not?
|
113
|
+
if( values[j] != null )
|
114
|
+
o.attributes[column] = values[j]
|
115
|
+
}
|
116
|
+
}
|
117
|
+
records.push(o)
|
118
|
+
}
|
119
|
+
}
|
120
|
+
return records
|
121
|
+
}
|
122
|
+
|
123
|
+
|
124
|
+
// change to update() or such - why? why not?
|
125
|
+
next() {
|
126
|
+
const sql = this._build_query()
|
127
|
+
const result = this.db.exec( sql )
|
128
|
+
const records = this._build_records( result )
|
129
|
+
|
130
|
+
let html = ""
|
131
|
+
if (records.length === 0) {
|
132
|
+
html = "No results"
|
133
|
+
} else {
|
134
|
+
console.log(records)
|
135
|
+
html = records.map((rec) => {
|
136
|
+
let attributes = rec.attributes
|
137
|
+
let keys = Object.keys(attributes)
|
138
|
+
// note: use "" for attribute quotes
|
139
|
+
// to allow single-quotes in values e.g. Wizard's Hat etc.
|
140
|
+
let table = keys.map((key) => {
|
141
|
+
return `<tr class="row" data-key="${key}"
|
142
|
+
data-val="${attributes[key]}">
|
143
|
+
<td>${key}</td>
|
144
|
+
<td>${attributes[key]}</td>
|
145
|
+
</tr>`
|
146
|
+
}).join("")
|
147
|
+
|
148
|
+
let img = rec.image
|
149
|
+
|
150
|
+
return `<div class="item">
|
151
|
+
<img src="${img}">
|
152
|
+
<table>${table}</table>
|
153
|
+
</div>`
|
154
|
+
}).join("")
|
155
|
+
}
|
156
|
+
|
157
|
+
document.querySelector(".container").innerHTML = html
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
@@ -1,18 +1,137 @@
|
|
1
1
|
body {
|
2
2
|
font-family: sans-serif;
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
margin: 0;
|
4
|
+
background: rgba(0,0,255,0.04);
|
5
|
+
}
|
6
|
+
nav {
|
7
|
+
text-align: center;
|
8
|
+
padding: 40px 0 20px 0;
|
9
|
+
color: black;
|
10
|
+
opacity: 0.9;
|
11
|
+
}
|
12
|
+
.sub {
|
13
|
+
font-size: 14px;
|
14
|
+
}
|
15
|
+
input[type=text] {
|
16
|
+
width: 100%;
|
17
|
+
padding: 5px;
|
18
|
+
box-sizing: border-box;
|
19
|
+
background: white;
|
20
|
+
border: 1px solid rgba(0,0,0,0.1);
|
21
|
+
}
|
22
|
+
textarea {
|
23
|
+
width: 100%;
|
24
|
+
height: 50px;
|
25
|
+
background: white;
|
26
|
+
border: 1px solid rgba(0,0,0,0.1);
|
27
|
+
padding: 5px;
|
28
|
+
box-sizing: border-box;
|
29
|
+
}
|
30
|
+
nav a {
|
31
|
+
font-family: sans-serif;
|
32
|
+
/*
|
33
|
+
letter-spacing: -3px;
|
34
|
+
*/
|
7
35
|
text-decoration: none;
|
8
|
-
|
9
|
-
|
36
|
+
font-size: 40px;
|
37
|
+
font-weight: bold;
|
38
|
+
display: inline-block;
|
39
|
+
margin: 10px;
|
40
|
+
color: black;
|
41
|
+
}
|
42
|
+
nav td:nth-child(1) {
|
43
|
+
width: 100px;
|
44
|
+
font-weight: bold;
|
45
|
+
text-align: right;
|
46
|
+
vertical-align: middle;
|
47
|
+
}
|
48
|
+
nav table {
|
49
|
+
max-width: 800px;
|
50
|
+
margin: 0 auto;
|
51
|
+
width: 90%;
|
52
|
+
}
|
53
|
+
nav td {
|
54
|
+
background: none;
|
55
|
+
color: black;
|
56
|
+
}
|
57
|
+
|
58
|
+
|
59
|
+
.container {
|
60
|
+
display: flex;
|
61
|
+
flex-wrap: wrap;
|
62
|
+
justify-content: center;
|
63
|
+
padding: 20px 0;
|
64
|
+
}
|
65
|
+
.item {
|
66
|
+
width: 200px;
|
67
|
+
padding: 5px;
|
68
|
+
box-sizing: border-box;
|
69
|
+
}
|
70
|
+
.item img {
|
71
|
+
width: 100%;
|
72
|
+
image-rendering: pixelated;
|
73
|
+
/* to phunk or not phunk? */
|
74
|
+
/* transform: scale(-1,1); */
|
75
|
+
}
|
76
|
+
table {
|
77
|
+
table-layout: fixed;
|
78
|
+
width: 100%;
|
79
|
+
border-spacing: 0;
|
80
|
+
}
|
81
|
+
td {
|
82
|
+
background: white;
|
83
|
+
vertical-align: top;
|
84
|
+
word-wrap:break-word;
|
85
|
+
padding: 7px;
|
86
|
+
box-sizing: border-box;
|
87
|
+
font-size: 13px;
|
88
|
+
cursor: pointer;
|
89
|
+
}
|
90
|
+
.container td:nth-child(1) {
|
91
|
+
font-weight: bold;
|
92
|
+
}
|
93
|
+
.container td {
|
94
|
+
border-bottom: 2px solid rgba(0,0,255,0.04);
|
95
|
+
}
|
96
|
+
.container tr:hover td {
|
97
|
+
background: lavender;
|
98
|
+
}
|
99
|
+
|
10
100
|
|
11
|
-
a:hover {
|
12
|
-
text-decoration: underline;
|
13
|
-
/* color: maroon; */
|
14
|
-
}
|
15
101
|
|
102
|
+
#query {
|
103
|
+
margin-top: 5px;
|
104
|
+
background: royalblue;
|
105
|
+
cursor: pointer;
|
106
|
+
padding: 10px;
|
107
|
+
max-width: 800px;
|
108
|
+
width: 90%;
|
109
|
+
box-sizing: border-box;
|
110
|
+
color: white;
|
111
|
+
border-radius: 4px;
|
112
|
+
border:none;
|
113
|
+
font-size: 20px;
|
114
|
+
font-family: sans-serif;
|
115
|
+
}
|
116
|
+
.btn {
|
117
|
+
font-family: sans-serif;
|
118
|
+
font-size: 14px;
|
119
|
+
font-style: normal;
|
120
|
+
letter-spacing: 0;
|
121
|
+
background: rgba(0,0,0,0.9);
|
122
|
+
color: white;
|
123
|
+
padding: 5px 10px;
|
124
|
+
margin: 20px 0;
|
125
|
+
border-radius: 2px;
|
126
|
+
}
|
127
|
+
|
128
|
+
.loader {
|
129
|
+
padding: 100px;
|
130
|
+
font-size: 14px;
|
131
|
+
opacity: 0.9;
|
132
|
+
font-weight: bold;
|
133
|
+
text-transform: uppercase;
|
134
|
+
}
|
16
135
|
|
17
136
|
|
18
137
|
/** version block **********/
|
data/lib/artserve/service.rb
CHANGED
@@ -1,7 +1,19 @@
|
|
1
1
|
|
2
2
|
# todo/check: find a better name?
|
3
3
|
class Artserve < Sinatra::Base
|
4
|
+
|
5
|
+
get '/artbase.db' do
|
6
|
+
path = settings.artbase
|
7
|
+
puts " serving sqlite database as (binary) blob >#{path}<..."
|
8
|
+
headers( 'Content-Type' => "application/octet-stream" )
|
9
|
+
|
10
|
+
blob = File.open( path, 'rb' ) { |f| f.read }
|
11
|
+
puts " #{blob.size} byte(s)"
|
12
|
+
blob
|
13
|
+
end
|
14
|
+
|
4
15
|
get '/' do
|
5
16
|
erb :index
|
6
17
|
end
|
18
|
+
|
7
19
|
end # class ProfilepicService
|
data/lib/artserve/version.rb
CHANGED
@@ -1,6 +1,75 @@
|
|
1
1
|
|
2
2
|
|
3
|
-
<h1>Artserve</h1>
|
4
3
|
|
5
|
-
<
|
6
|
-
|
4
|
+
<nav>
|
5
|
+
<a href="/"><%= settings.artbase %></a>
|
6
|
+
<div class='sub'>Yes, you can! Query the entire
|
7
|
+
art collection (metadata & images)
|
8
|
+
in a single sqlite database (file)
|
9
|
+
with a "serverless" web page.
|
10
|
+
</div>
|
11
|
+
<div>
|
12
|
+
<a class='btn' href="/artbase.db">Download</a>
|
13
|
+
<a class='btn' href="https://old.reddit.com/r/DIYPunkArt/">Questions? Comments?</a>
|
14
|
+
</div>
|
15
|
+
<table>
|
16
|
+
<tr>
|
17
|
+
<td>SELECT</td>
|
18
|
+
<td><input id='select' type='text' placeholder='select columns' value='*'></td>
|
19
|
+
</tr>
|
20
|
+
<tr>
|
21
|
+
<td>FROM</td>
|
22
|
+
<td>metadata</td>
|
23
|
+
</tr>
|
24
|
+
<tr>
|
25
|
+
<td>WHERE</td>
|
26
|
+
<td><textarea id='where' placeholder='where condition'></textarea></td>
|
27
|
+
</tr>
|
28
|
+
<tr>
|
29
|
+
<td>LIMIT</td>
|
30
|
+
<td><input id='limit' type='text' placeholder='limmit' value=200></td>
|
31
|
+
</tr>
|
32
|
+
</table>
|
33
|
+
<button id='query'>Query</button>
|
34
|
+
</nav>
|
35
|
+
|
36
|
+
|
37
|
+
<div class='container'>
|
38
|
+
<div class='loader'>
|
39
|
+
<div class='loading dots'></div>
|
40
|
+
<br>
|
41
|
+
<div>Loading ...</div>
|
42
|
+
</div>
|
43
|
+
</div>
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
<script>
|
49
|
+
document.addEventListener("DOMContentLoaded", async () => {
|
50
|
+
const artbase = new Artbase()
|
51
|
+
await artbase.init()
|
52
|
+
|
53
|
+
artbase.next()
|
54
|
+
|
55
|
+
|
56
|
+
document.querySelector("#query").addEventListener("click", () => {
|
57
|
+
artbase.next()
|
58
|
+
})
|
59
|
+
|
60
|
+
document.querySelector(".container").addEventListener("click", (e) => {
|
61
|
+
let target = (e.target.className === "row" ? e.target : (e.target.closest(".row") ? e.target.closest(".row") : null))
|
62
|
+
if (target) {
|
63
|
+
let key = target.getAttribute('data-key')
|
64
|
+
let val = target.getAttribute('data-val')
|
65
|
+
let where = document.querySelector("#where").value
|
66
|
+
if (where.trim().length > 0) {
|
67
|
+
document.querySelector("#where").value = `${where} AND\n${key} = "${val}"`
|
68
|
+
} else {
|
69
|
+
document.querySelector("#where").value = `${key} = "${val}"`
|
70
|
+
}
|
71
|
+
artbase.next()
|
72
|
+
}
|
73
|
+
})
|
74
|
+
})
|
75
|
+
</script>
|
data/lib/artserve.rb
CHANGED
@@ -10,9 +10,22 @@ require_relative 'artserve/service'
|
|
10
10
|
|
11
11
|
|
12
12
|
|
13
|
-
class Artserve
|
14
|
-
def self.main
|
15
|
-
puts 'hello from main'
|
13
|
+
class Artserve ## note: Artserve is Sinatra::Base
|
14
|
+
def self.main( argv=ARGV )
|
15
|
+
puts 'hello from main with args:'
|
16
|
+
pp argv
|
17
|
+
|
18
|
+
path = argv[0] || './artbase.db'
|
19
|
+
|
20
|
+
## if passed in a directory, auto-add /artbase.db for now - why? why not
|
21
|
+
path += "/artbase.db" if Dir.exist?( path )
|
22
|
+
|
23
|
+
|
24
|
+
puts " using database: >#{path}<"
|
25
|
+
|
26
|
+
## note: let's you use latter settings.artbase (resulting in path)
|
27
|
+
self.set( :artbase, path )
|
28
|
+
|
16
29
|
|
17
30
|
#####
|
18
31
|
# fix/todo:
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: artserve
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gerald Bauer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-09-
|
11
|
+
date: 2022-09-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sinatra
|
@@ -89,6 +89,7 @@ files:
|
|
89
89
|
- Rakefile
|
90
90
|
- bin/artserve
|
91
91
|
- lib/artserve.rb
|
92
|
+
- lib/artserve/public/artbase.js
|
92
93
|
- lib/artserve/public/style.css
|
93
94
|
- lib/artserve/service.rb
|
94
95
|
- lib/artserve/version.rb
|