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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee35cbcad011ec2401a4f4f2721085fb446f59a90b4bd8d44639fa9664fc1aa3
4
- data.tar.gz: c463a6104dce4ce3fcc4ddbd15ad3aeb9e49337c8da0f02e89e26b3e63bb078d
3
+ metadata.gz: 60a1c24aca28429732b6b6b90a2008b3abd8d56e5e7849175c7a16701b7dbdbf
4
+ data.tar.gz: f8c745e129b3d4b421f5681056b5dd6d8507244ef69c5f40cdee56a0e4ae4b05
5
5
  SHA512:
6
- metadata.gz: a2b2d6e2bd48a9a8a6cc8eebc8d480462917fe1e677172077f55f94851537f1a28f47f3e356f39edf8a66117609744c87dd69576038359ec3ddd88887d420d43
7
- data.tar.gz: ca5a7940eeaac3a47baa5213090dc67f5acfdf8ca8204b3ea0594d4a0617fa19d2ebe0578a824c54f3d0a680529a9b391a3dbec66a5ff4521ab139793414e78d
6
+ metadata.gz: a48553cf45dff026e510acfa29aaa7af1d10623807233ca10a2bc1b5cb3b0ff9320c30194b0efce64d20df7ec27cb39ec6da9e08c2277b8e0b471ec9b619ab7d
7
+ data.tar.gz: 6bc157bfdab797976939144dde837de49aa98d9b3dfb9c0ad180f33e1ae44986981e184715bb8841c1f4ea361b4904f5ac2208346d963eff8fefe1f5a7f3402b
data/Manifest.txt CHANGED
@@ -4,6 +4,7 @@ README.md
4
4
  Rakefile
5
5
  bin/artserve
6
6
  lib/artserve.rb
7
+ lib/artserve/public/artbase.js
7
8
  lib/artserve/public/style.css
8
9
  lib/artserve/service.rb
9
10
  lib/artserve/version.rb
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
- To be done
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
- color: #333333;
4
- }
5
-
6
- a, a:visited {
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
- /* color: maroon; */
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 **********/
@@ -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
@@ -3,8 +3,8 @@ module Artbase
3
3
  module Module
4
4
  module Artserve
5
5
  MAJOR = 0
6
- MINOR = 0
7
- PATCH = 1
6
+ MINOR = 1
7
+ PATCH = 0
8
8
  VERSION = [MAJOR,MINOR,PATCH].join('.')
9
9
 
10
10
  def self.version
@@ -1,6 +1,75 @@
1
1
 
2
2
 
3
- <h1>Artserve</h1>
4
3
 
5
- <p>Hello.
6
- </p>
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>
@@ -3,9 +3,10 @@
3
3
  <head>
4
4
  <meta charset='utf-8'>
5
5
 
6
- <title>Artserve</title>
6
+ <title>Artbase</title>
7
7
 
8
8
  <link href="<%= url('/style.css') %>" rel='stylesheet'>
9
+ <script src="/artbase.js"></script>
9
10
  </head>
10
11
  <body>
11
12
 
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.1
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-19 00:00:00.000000000 Z
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