artserve 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|