commit 7cb1f0242377c8414eddaca2250b22ddbf3b9c6f Author: mappu Date: Sun Jul 9 11:13:36 2017 +1200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbab7f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Binaries +cmd/yatwiki-server/yatwiki-server + +# Development db files +cmd/yatwiki-server/wiki.db diff --git a/DB.go b/DB.go new file mode 100644 index 0000000..1b7fe79 --- /dev/null +++ b/DB.go @@ -0,0 +1,146 @@ +package yatwiki3 + +import ( + "crypto/md5" + "database/sql" + "encoding/hex" + "fmt" + "net/http" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +type WikiDB struct { + db *sql.DB +} + +func NewWikiDB(dbFilePath string) (*WikiDB, error) { + db, err := sql.Open("sqlite3", dbFilePath) + if err != nil { + return nil, err + } + + wdb := WikiDB{db: db} + err = wdb.assertSchema() + if err != nil { + return nil, fmt.Errorf("assertSchema: %s", err.Error()) + } + + return &wdb, nil +} + +func (this *WikiDB) assertSchema() error { + _, err := this.db.Exec(` + CREATE TABLE IF NOT EXISTS articles ( + id INTEGER PRIMARY KEY, + article INTEGER, + modified INTEGER, + body BLOB, + author TEXT + );`) + if err != nil { + return err + } + + _, err = this.db.Exec(` + CREATE TABLE IF NOT EXISTS titles ( + id INTEGER PRIMARY KEY, + title TEXT + );`) + if err != nil { + return err + } + + _, err = this.db.Exec(` + CREATE INDEX IF NOT EXISTS articles_modified_index ON articles (modified) + `) + if err != nil { + return err + } + + _, err = this.db.Exec(` + CREATE INDEX IF NOT EXISTS articles_title_index ON articles (article) + `) + if err != nil { + return err + } + + return nil +} + +type Article struct { + ID int + TitleID int + Modified int64 + Body []byte + Author string +} + +func (this *Article) FillModifiedTimestamp() { + this.Modified = time.Now().Unix() +} + +func (this *Article) FillAuthor(r *http.Request) { + userAgentHash := md5.Sum([]byte(r.UserAgent())) + this.Author = r.RemoteAddr + "-" + hex.EncodeToString(userAgentHash[:])[:6] +} + +type ArticleWithTitle struct { + Article + Title string +} + +func (this *WikiDB) GetArticleById(articleId int) (*Article, error) { + row := this.db.QueryRow(`SELECT articles.* FROM articles WHERE id = ?`, articleId) + return this.parseArticle(row) +} + +func (this *WikiDB) GetLatestVersion(title string) (*Article, error) { + row := this.db.QueryRow(`SELECT articles.* FROM articles WHERE article = (SELECT id FROM titles WHERE title = ?) ORDER BY modified DESC LIMIT 1`, title) + return this.parseArticle(row) +} + +func (this *WikiDB) ListTitles() ([]string, error) { + rows, err := this.db.Query(`SELECT title FROM titles ORDER BY title ASC`) + if err != nil { + return nil, err + } + + defer rows.Close() + ret := make([]string, 0) + for rows.Next() { + var title string + err = rows.Scan(&title) + if err != nil { + return nil, err + } + + ret = append(ret, title) + } + return ret, nil +} + +func (this *WikiDB) parseArticle(row *sql.Row) (*Article, error) { + a := Article{} + err := row.Scan(&a.ID, &a.TitleID, &a.Modified, &a.Body, &a.Author) + if err != nil { + return nil, err + } + + return &a, nil +} + +func (this *WikiDB) parseArticleWithTitle(row *sql.Row) (*ArticleWithTitle, error) { + a := ArticleWithTitle{} + err := row.Scan(&a.ID, &a.TitleID, &a.Modified, &a.Body, &a.Author, &a.Title) + if err != nil { + return nil, err + } + + return &a, nil +} + +func (this *WikiDB) Close() { + this.db.Close() +} diff --git a/ServerOptions.go b/ServerOptions.go new file mode 100644 index 0000000..f082e07 --- /dev/null +++ b/ServerOptions.go @@ -0,0 +1,31 @@ +package yatwiki3 + +import ( + "time" +) + +type ServerOptions struct { + PageTitle string + ExpectBaseURL string + DefaultPage string + Timezone string + DateFormat string + DBFilePath string + AllowDBDownload bool + RecentChanges int + GzipCompressionLevel int +} + +func DefaultOptions() *ServerOptions { + return &ServerOptions{ + PageTitle: "YATWiki", + ExpectBaseURL: "/", + DefaultPage: "home", + Timezone: "UTC", + DateFormat: time.RFC822Z, + DBFilePath: "wiki.db", + AllowDBDownload: true, + RecentChanges: 20, + GzipCompressionLevel: 9, + } +} diff --git a/WikiServer.go b/WikiServer.go new file mode 100644 index 0000000..43c7b9f --- /dev/null +++ b/WikiServer.go @@ -0,0 +1,59 @@ +package yatwiki3 + +import ( + "html/template" + "log" + "net/http" +) + +type WikiServer struct { + db *WikiDB + opts *ServerOptions + pageTmp *template.Template +} + +func NewWikiServer(opts *ServerOptions) (*WikiServer, error) { + wdb, err := NewWikiDB(opts.DBFilePath) + if err != nil { + return nil, err + } + + tmpl, err := template.New("yatwiki/page").Parse(pageTemplate) + if err != nil { + panic(err) + } + + ws := WikiServer{ + db: wdb, + opts: opts, + pageTmp: tmpl, + } + return &ws, nil +} + +func (this *WikiServer) Close() { + this.db.Close() +} + +func (this *WikiServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Server", "Yatwiki3") + + if r.URL.Path == this.opts.ExpectBaseURL+"wiki.css" { + w.Header().Set("Content-Type", "text/css") + w.Write(tmplWikiCss) + return + } + + pto := DefaultPageTemplateOptions(this.opts) + pto.SessionMessage = `Invalid request.` + //pto.CurrentPageIsArticle = true + //pto.CurrentPageName = "quotes/\"2017" + this.servePageResponse(w, pto) +} + +func (this *WikiServer) servePageResponse(w http.ResponseWriter, pto *pageTemplateOptions) { + err := this.pageTmp.Execute(w, pto) + if err != nil { + log.Println(err.Error()) + } +} diff --git a/cmd/yatwiki-server/main.go b/cmd/yatwiki-server/main.go new file mode 100644 index 0000000..e150e0d --- /dev/null +++ b/cmd/yatwiki-server/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "fmt" + "net/http" + "os" + + "code.ivysaur.me/yatwiki3" +) + +func main() { + + bindAddr := flag.String("listen", "127.0.0.1:80", "Bind address") + dbPath := flag.String("database", "wiki.db", "Database file") + flag.Parse() + + opts := yatwiki3.DefaultOptions() + opts.DBFilePath = *dbPath + + ws, err := yatwiki3.NewWikiServer(opts) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + defer ws.Close() + + err = http.ListenAndServe(*bindAddr, ws) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + + os.Exit(0) +} diff --git a/pageTemplate.go b/pageTemplate.go new file mode 100644 index 0000000..572cc0c --- /dev/null +++ b/pageTemplate.go @@ -0,0 +1,113 @@ +package yatwiki3 + +import ( + "fmt" + "html/template" + "time" +) + +var subresourceNonce = time.Now().Unix() + +type pageTemplateOptions struct { + CurrentPageIsArticle bool + CurrentPageName string + WikiTitle string + Content template.HTML + BaseURL string + LoadCodeResources bool + DefaultPage string + AllowDownload bool + SessionMessage template.HTML +} + +func DefaultPageTemplateOptions(opts *ServerOptions) *pageTemplateOptions { + return &pageTemplateOptions{ + WikiTitle: opts.PageTitle, + BaseURL: opts.ExpectBaseURL, + DefaultPage: opts.DefaultPage, + AllowDownload: opts.AllowDBDownload, + } +} + +func (this *pageTemplateOptions) NewArticleTitle() string { + return fmt.Sprintf("untitled-%d", time.Now().Unix()) +} + +func (this *pageTemplateOptions) SubresourceNonce() int64 { + return subresourceNonce +} + +const pageTemplate string = ` + + + {{.CurrentPageName}}{{ if len .CurrentPageName }} - {{end}}{{.WikiTitle}} + + + + + +{{if .LoadCodeResources}} + + +{{end}} + + + +
+
+
+
+{{if .CurrentPageIsArticle }} +
+
+
+{{end}} +
+ + + +
+{{if len .SessionMessage}} +
{{.SessionMessage}}
+{{end}} +{{.Content}} +
+ +` diff --git a/tmpl_WikiCss.go b/tmpl_WikiCss.go new file mode 100644 index 0000000..6fa5464 --- /dev/null +++ b/tmpl_WikiCss.go @@ -0,0 +1,279 @@ +package yatwiki3 + +var tmplWikiCss []byte = []byte(` +/* wiki.css */ +html,body { + background:white;color:black;font-size:12px; + margin:0;padding:0;border:0; +} +html,body,input{font-family:Verdana,Arial;} +table,tr,td,th {border:0px;} +input {font-size:8px;} +a {text-decoration:none;color: blue;} +a:hover {text-decoration:underline;} +img {border:0px;max-width:100%;} +ul {margin:0px; padding-left:30px;} +h2 {display:inline;} +pre {font-family:Consolas,Courier,monospace;font-size:11px;} +td {padding:0px 10px;} +.content{padding:8px;} +.s {text-decoration:line-through;} +.spoiler{color:black;background-color:black;} +.spoiler:hover{color:white;} + +.imgur { + border:1px solid white; + width:90px; + height:90px; + opacity:0.6; + -moz-transition:all 0.1s linear; + -webkit-transition:all 0.1s linear; +} +.imgur:hover {opacity: 1.0;} + +/* Header */ +.header { + background:#DDD; + padding:3px; + font-size:0px; + box-shadow: 0px 4px 24px #CCC; + /*position:absolute; + left:0;right:0;*/ +} +.header a { + background:#DDD; + color:grey; + text-decoration: none; + margin:0px 2px; + border:1px solid #DDD; + display:inline-block; + height:16px; + padding:2px 3px; + -moz-transition:all 0.1s linear; + -webkit-transition:all 0.1s linear; +} +.header a:hover { + background:white; + color:black; + border:1px solid black; + border-color:grey black black grey; +} +.info { + display:block; + border:1px solid darkgrey; + padding:2px 4px; + margin:10px 5px; + color:black; + background-color:lightyellow; +} + +/* Editor page */ +fieldset {border:1px solid grey;} +fieldset legend { + padding:2px 6px 2px 6px; + background:#DDD; + border:1px solid grey; + font-weight:bold; +} +.editor { + padding:0px; + margin:0px; + vertical-align:top; +} +#contentctr { + border:1px dashed lightgrey; + position:absolute; + top:120px;right:10px;bottom:10px;left:10px; +} +.editor textarea { + font-family:Consolas,Courier,monospace;font-size:10px; + margin:0;padding:0;border:0; + + width:100%;height:100%; + min-width:100%;min-height:100%; /* no resize */ + max-width:100%;max-height:100%; + position:absolute; /* IE7 */ +} +.frm { + border:1px solid #DDD; + background:#EEE; + font-size:10px; + padding:5px; + margin:5px 10px 10px 15px; +} + +/* Tables in content */ +.ti {border-collapse: collapse; border-style:hidden;} +.ti td {border-left:1px solid #DDD;border-right:1px solid #DDD;} +.ti tr:first-child {font-weight:bold;} +.ti tr:first-child td {border-bottom:1px solid #DDD;} +.ti tr:hover {background-color:#F8F8F8;} +.ti tr:first-child:hover{background-color:white;} + +/* Sprites */ +.sprite { + display:inline-block; + width:16px;height:16px; + vertical-align:text-bottom; + background-repeat:no-repeat; + background-image: url(data:image/png;base64,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); +} +.sprite.hm { background-position:0px 0px;} +.sprite.hs { background-position:0px -16px;} +.sprite.sp { background-position:0px -32px;} +.sprite.nw { background-position:-16px 0px;} +.sprite.ed { background-position:-16px -16px;} +.sprite.rn { background-position:-16px -32px;} +.sprite.no { + background:none; +} +.sep { + display:inline-block; + *display:inline; /* IE7 */ + zoom: 1; /* IE7 */ + height:16px; + width:0px; + border-left:1px solid darkgrey; + border-right:1px solid #DDD; + margin:0px 4px; +} +/* Sections */ +.section { + display:inline-block; + background:#F8F8F8; + width:auto; + padding:3px; + border:1px dashed #DDD; +} +.sectionheader { + color:green; + font-weight:bold; +} +/* Dropdown */ +.ddmenu { + display:block; + position:absolute; + top:28px; + left:32px; + width:180px; + border-top:1px solid #999; + box-shadow: 0px 4px 24px #CCC; + z-index: 2; +} +.ddmenu a { + color:black; + display:inline-block; + border:1px solid #999; + border-top:0px; + width:170px; + padding:4px; + background:#EEE; +} +.ddmenu a:hover { + text-decoration:none;background:#FFF; + -moz-transition:all 0.1s linear; + -webkit-transition:all 0.1s linear; +} +#tr1 { + width:0px;height:0px;border-bottom:10px solid #999; + border-right:10px solid transparent;border-left:10px solid transparent; + position:absolute;top:18px;left:35px;z-index:1; +} +#tr2 { + width:0px;height:0px;border-bottom:10px solid #EEE; + border-right:10px solid transparent;border-left:10px solid transparent; + position:absolute;top:19px;left:35px;z-index:3; +} +/* Diffs, rawhtml */ +del{text-decoration:none;background:red;font-weight:bold;} +ins{text-decoration:none;background:lightgreen;font-weight:bold;} +.html a {color:red;font-weight:bold;} + +/* + +highlight.css +Visual Studio-like style based on original C# coloring by Jason Diamond + +*/ +pre code { + display: block; padding: 0.5em; +} + +pre .comment, +pre .annotation, +pre .template_comment, +pre .diff .header, +pre .chunk, +pre .apache .cbracket { + color: rgb(0, 128, 0); +} + +pre .keyword, +pre .id, +pre .built_in, +pre .smalltalk .class, +pre .winutils, +pre .bash .variable, +pre .tex .command, +pre .request, +pre .status, +pre .nginx .title, +pre .xml .tag, +pre .xml .tag .value { + color: rgb(0, 0, 255); +} + +pre .string, +pre .title, +pre .parent, +pre .tag .value, +pre .rules .value, +pre .rules .value .number, +pre .ruby .symbol, +pre .ruby .symbol .string, +pre .aggregate, +pre .template_tag, +pre .django .variable, +pre .addition, +pre .flow, +pre .stream, +pre .apache .tag, +pre .date, +pre .tex .formula { + color: rgb(163, 21, 21); +} + +pre .ruby .string, +pre .decorator, +pre .filter .argument, +pre .localvars, +pre .array, +pre .attr_selector, +pre .pseudo, +pre .pi, +pre .doctype, +pre .deletion, +pre .envvar, +pre .shebang, +pre .preprocessor, +pre .userType, +pre .apache .sqbracket, +pre .nginx .built_in, +pre .tex .special, +pre .prompt { + color: rgb(43, 145, 175); +} + +pre .phpdoc, +pre .javadoc, +pre .xmlDocTag { + color: rgb(128, 128, 128); +} + +pre .vhdl .typename { font-weight: bold; } +pre .vhdl .string { color: #666666; } +pre .vhdl .literal { color: rgb(163, 21, 21); } +pre .vhdl .attribute { color: #00B0E8; } + +pre .xml .attribute { color: rgb(255, 0, 0); } +`)