├── .codeclimate.yml ├── .dir-locals.el ├── .dockerignore ├── .env.test ├── .ghci ├── .gitignore ├── .rubocop.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── app ├── DevelMain.hs ├── devel.hs ├── main-worker.hs └── main.hs ├── bin └── example ├── circle.yml ├── config ├── favicon.ico ├── keter.yml ├── models ├── robots.txt ├── routes ├── settings.yml └── sqlite.yml ├── docker-compose.yml ├── render.yaml ├── sdk └── ruby │ ├── .ruby-version │ ├── Gemfile │ ├── Gemfile.lock │ ├── README.md │ ├── Rakefile │ ├── VERSION │ ├── example.rb │ ├── lib │ ├── tee_io.rb │ └── tee_io │ │ ├── api.rb │ │ ├── process.rb │ │ └── token_response.rb │ ├── spec │ ├── spec_helper.rb │ ├── tee_io │ │ ├── api_spec.rb │ │ ├── process_spec.rb │ │ └── token_response_spec.rb │ └── tee_io_spec.rb │ └── tee-io.gemspec ├── src ├── Application.hs ├── Archive.hs ├── CommandContent.hs ├── Data │ └── Time │ │ └── Duration.hs ├── Foundation.hs ├── Handler │ ├── Command.hs │ ├── Common.hs │ ├── Home.hs │ └── Output.hs ├── Import.hs ├── Import │ └── NoFoundation.hs ├── Model.hs ├── Network │ ├── PGDatabaseURL.hs │ └── S3URL.hs ├── Settings.hs ├── Settings │ └── StaticFiles.hs ├── Token.hs └── Worker.hs ├── stack.yaml ├── stack.yaml.lock ├── static ├── css │ └── screen.css ├── demo.gif ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff └── tee-io ├── tee-io.cabal ├── templates ├── command.hamlet ├── command.julius ├── default-layout-wrapper.hamlet ├── default-layout.hamlet └── homepage.hamlet └── test ├── Data └── Time │ └── DurationSpec.hs ├── Handler ├── CommandSpec.hs ├── HomeSpec.hs └── OutputSpec.hs ├── Network ├── PGDatabaseURLSpec.hs └── S3URLSpec.hs ├── Spec.hs ├── SpecHelper.hs └── WorkerSpec.hs /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | hlint: 4 | enabled: true 5 | fixme: 6 | enabled: true 7 | rubocop: 8 | enabled: true 9 | ratings: 10 | paths: 11 | - "**.hs" 12 | - "**.rb" 13 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((haskell-mode . ((haskell-indent-spaces . 4) 2 | (haskell-process-use-ghci . t))) 3 | (hamlet-mode . ((hamlet/basic-offset . 4) 4 | (haskell-process-use-ghci . t)))) 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .stack-work 3 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | AWS_ACCESS_KEY_ID=x 2 | AWS_SECRET_ACCESS_KEY=x 3 | DATABASE_URL=postgres://postgres:postgres@localhost:5432/teeio_test 4 | LOG_LEVEL=error 5 | S3_URL=http://localhost:4569/tee.io.test 6 | -------------------------------------------------------------------------------- /.ghci: -------------------------------------------------------------------------------- 1 | :set -DDEVELOPMENT 2 | :set -XCPP 3 | :set -XDeriveDataTypeable 4 | :set -XEmptyDataDecls 5 | :set -XFlexibleContexts 6 | :set -XGADTs 7 | :set -XGeneralizedNewtypeDeriving 8 | :set -XMultiParamTypeClasses 9 | :set -XNoImplicitPrelude 10 | :set -XNoMonomorphismRestriction 11 | :set -XOverloadedStrings 12 | :set -XQuasiQuotes 13 | :set -XRecordWildCards 14 | :set -XTemplateHaskell 15 | :set -XTupleSections 16 | :set -XTypeFamilies 17 | :set -XViewPatterns 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | static/tmp/ 3 | static/combined/ 4 | config/client_session_key.aes 5 | .stack-work/ 6 | yesod-devel/ 7 | .env* 8 | !.env.test 9 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Disable all checks not explicitly referenced in this file 2 | # This is used to easily disable Style/* checks 3 | AllCops: 4 | DisabledByDefault: true 5 | 6 | ################## STYLE ################################# 7 | 8 | Style/AccessModifierIndentation: 9 | Description: Check indentation of private/protected visibility modifiers. 10 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-public-private-protected' 11 | Enabled: false 12 | 13 | Style/AccessorMethodName: 14 | Description: Check the naming of accessor methods for get_/set_. 15 | Enabled: false 16 | 17 | Style/Alias: 18 | Description: 'Use alias_method instead of alias.' 19 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method' 20 | Enabled: false 21 | 22 | Style/AlignArray: 23 | Description: >- 24 | Align the elements of an array literal if they span more than 25 | one line. 26 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays' 27 | Enabled: false 28 | 29 | Style/AlignHash: 30 | Description: >- 31 | Align the elements of a hash literal if they span more than 32 | one line. 33 | Enabled: false 34 | 35 | Style/AlignParameters: 36 | Description: >- 37 | Align the parameters of a method call if they span more 38 | than one line. 39 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent' 40 | Enabled: false 41 | 42 | Style/AndOr: 43 | Description: 'Use &&/|| instead of and/or.' 44 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-and-or-or' 45 | Enabled: false 46 | 47 | Style/ArrayJoin: 48 | Description: 'Use Array#join instead of Array#*.' 49 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#array-join' 50 | Enabled: false 51 | 52 | Style/AsciiComments: 53 | Description: 'Use only ascii symbols in comments.' 54 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments' 55 | Enabled: false 56 | 57 | Style/AsciiIdentifiers: 58 | Description: 'Use only ascii symbols in identifiers.' 59 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers' 60 | Enabled: false 61 | 62 | Style/Attr: 63 | Description: 'Checks for uses of Module#attr.' 64 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr' 65 | Enabled: false 66 | 67 | Style/BeginBlock: 68 | Description: 'Avoid the use of BEGIN blocks.' 69 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-BEGIN-blocks' 70 | Enabled: false 71 | 72 | Style/BarePercentLiterals: 73 | Description: 'Checks if usage of %() or %Q() matches configuration.' 74 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q-shorthand' 75 | Enabled: false 76 | 77 | Style/BlockComments: 78 | Description: 'Do not use block comments.' 79 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-block-comments' 80 | Enabled: false 81 | 82 | Style/BlockEndNewline: 83 | Description: 'Put end statement of multiline block on its own line.' 84 | Enabled: false 85 | 86 | Style/BlockDelimiters: 87 | Description: >- 88 | Avoid using {...} for multi-line blocks (multiline chaining is 89 | always ugly). 90 | Prefer {...} over do...end for single-line blocks. 91 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' 92 | Enabled: false 93 | 94 | Style/BracesAroundHashParameters: 95 | Description: 'Enforce braces style around hash parameters.' 96 | Enabled: false 97 | 98 | Style/CaseEquality: 99 | Description: 'Avoid explicit use of the case equality operator(===).' 100 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality' 101 | Enabled: false 102 | 103 | Style/CaseIndentation: 104 | Description: 'Indentation of when in a case/when/[else/]end.' 105 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-when-to-case' 106 | Enabled: false 107 | 108 | Style/CharacterLiteral: 109 | Description: 'Checks for uses of character literals.' 110 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-character-literals' 111 | Enabled: false 112 | 113 | Style/ClassAndModuleCamelCase: 114 | Description: 'Use CamelCase for classes and modules.' 115 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#camelcase-classes' 116 | Enabled: false 117 | 118 | Style/ClassAndModuleChildren: 119 | Description: 'Checks style of children classes and modules.' 120 | Enabled: false 121 | 122 | Style/ClassCheck: 123 | Description: 'Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.' 124 | Enabled: false 125 | 126 | Style/ClassMethods: 127 | Description: 'Use self when defining module/class methods.' 128 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#def-self-class-methods' 129 | Enabled: false 130 | 131 | Style/ClassVars: 132 | Description: 'Avoid the use of class variables.' 133 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-class-vars' 134 | Enabled: false 135 | 136 | Style/ClosingParenthesisIndentation: 137 | Description: 'Checks the indentation of hanging closing parentheses.' 138 | Enabled: false 139 | 140 | Style/ColonMethodCall: 141 | Description: 'Do not use :: for method call.' 142 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#double-colons' 143 | Enabled: false 144 | 145 | Style/CommandLiteral: 146 | Description: 'Use `` or %x around command literals.' 147 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-x' 148 | Enabled: false 149 | 150 | Style/CommentAnnotation: 151 | Description: >- 152 | Checks formatting of special comments 153 | (TODO, FIXME, OPTIMIZE, HACK, REVIEW). 154 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords' 155 | Enabled: false 156 | 157 | Style/CommentIndentation: 158 | Description: 'Indentation of comments.' 159 | Enabled: false 160 | 161 | Style/ConstantName: 162 | Description: 'Constants should use SCREAMING_SNAKE_CASE.' 163 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#screaming-snake-case' 164 | Enabled: false 165 | 166 | Style/DefWithParentheses: 167 | Description: 'Use def with parentheses when there are arguments.' 168 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' 169 | Enabled: false 170 | 171 | Style/DeprecatedHashMethods: 172 | Description: 'Checks for use of deprecated Hash methods.' 173 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-key' 174 | Enabled: false 175 | 176 | Style/Documentation: 177 | Description: 'Document classes and non-namespace modules.' 178 | Enabled: false 179 | 180 | Style/DotPosition: 181 | Description: 'Checks the position of the dot in multi-line method calls.' 182 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains' 183 | Enabled: false 184 | 185 | Style/DoubleNegation: 186 | Description: 'Checks for uses of double negation (!!).' 187 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-bang-bang' 188 | Enabled: false 189 | 190 | Style/EachWithObject: 191 | Description: 'Prefer `each_with_object` over `inject` or `reduce`.' 192 | Enabled: false 193 | 194 | Style/ElseAlignment: 195 | Description: 'Align elses and elsifs correctly.' 196 | Enabled: false 197 | 198 | Style/EmptyElse: 199 | Description: 'Avoid empty else-clauses.' 200 | Enabled: false 201 | 202 | Style/EmptyLineBetweenDefs: 203 | Description: 'Use empty lines between defs.' 204 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#empty-lines-between-methods' 205 | Enabled: false 206 | 207 | Style/EmptyLines: 208 | Description: "Don't use several empty lines in a row." 209 | Enabled: false 210 | 211 | Style/EmptyLinesAroundAccessModifier: 212 | Description: "Keep blank lines around access modifiers." 213 | Enabled: false 214 | 215 | Style/EmptyLinesAroundBlockBody: 216 | Description: "Keeps track of empty lines around block bodies." 217 | Enabled: false 218 | 219 | Style/EmptyLinesAroundClassBody: 220 | Description: "Keeps track of empty lines around class bodies." 221 | Enabled: false 222 | 223 | Style/EmptyLinesAroundModuleBody: 224 | Description: "Keeps track of empty lines around module bodies." 225 | Enabled: false 226 | 227 | Style/EmptyLinesAroundMethodBody: 228 | Description: "Keeps track of empty lines around method bodies." 229 | Enabled: false 230 | 231 | Style/EmptyLiteral: 232 | Description: 'Prefer literals to Array.new/Hash.new/String.new.' 233 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash' 234 | Enabled: false 235 | 236 | Style/EndBlock: 237 | Description: 'Avoid the use of END blocks.' 238 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks' 239 | Enabled: false 240 | 241 | Style/EndOfLine: 242 | Description: 'Use Unix-style line endings.' 243 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf' 244 | Enabled: false 245 | 246 | Style/EvenOdd: 247 | Description: 'Favor the use of Fixnum#even? && Fixnum#odd?' 248 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' 249 | Enabled: false 250 | 251 | Style/ExtraSpacing: 252 | Description: 'Do not use unnecessary spacing.' 253 | Enabled: false 254 | 255 | Style/FileName: 256 | Description: 'Use snake_case for source file names.' 257 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' 258 | Enabled: false 259 | 260 | Style/InitialIndentation: 261 | Description: >- 262 | Checks the indentation of the first non-blank non-comment line in a file. 263 | Enabled: false 264 | 265 | Style/FirstParameterIndentation: 266 | Description: 'Checks the indentation of the first parameter in a method call.' 267 | Enabled: false 268 | 269 | Style/FlipFlop: 270 | Description: 'Checks for flip flops' 271 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops' 272 | Enabled: false 273 | 274 | Style/For: 275 | Description: 'Checks use of for or each in multiline loops.' 276 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-for-loops' 277 | Enabled: false 278 | 279 | Style/FormatString: 280 | Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.' 281 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf' 282 | Enabled: false 283 | 284 | Style/GlobalVars: 285 | Description: 'Do not introduce global variables.' 286 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#instance-vars' 287 | Reference: 'http://www.zenspider.com/Languages/Ruby/QuickRef.html' 288 | Enabled: false 289 | 290 | Style/GuardClause: 291 | Description: 'Check for conditionals that can be replaced with guard clauses' 292 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals' 293 | Enabled: false 294 | 295 | Style/HashSyntax: 296 | Description: >- 297 | Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax 298 | { :a => 1, :b => 2 }. 299 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-literals' 300 | Enabled: false 301 | 302 | Style/IfUnlessModifier: 303 | Description: >- 304 | Favor modifier if/unless usage when you have a 305 | single-line body. 306 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier' 307 | Enabled: false 308 | 309 | Style/IfWithSemicolon: 310 | Description: 'Do not use if x; .... Use the ternary operator instead.' 311 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs' 312 | Enabled: false 313 | 314 | Style/IndentationConsistency: 315 | Description: 'Keep indentation straight.' 316 | Enabled: false 317 | 318 | Style/IndentationWidth: 319 | Description: 'Use 2 spaces for indentation.' 320 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' 321 | Enabled: false 322 | 323 | Style/IndentArray: 324 | Description: >- 325 | Checks the indentation of the first element in an array 326 | literal. 327 | Enabled: false 328 | 329 | Style/IndentHash: 330 | Description: 'Checks the indentation of the first key in a hash literal.' 331 | Enabled: false 332 | 333 | Style/InfiniteLoop: 334 | Description: 'Use Kernel#loop for infinite loops.' 335 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#infinite-loop' 336 | Enabled: false 337 | 338 | Style/Lambda: 339 | Description: 'Use the new lambda literal syntax for single-line blocks.' 340 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#lambda-multi-line' 341 | Enabled: false 342 | 343 | Style/LambdaCall: 344 | Description: 'Use lambda.call(...) instead of lambda.(...).' 345 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc-call' 346 | Enabled: false 347 | 348 | Style/LeadingCommentSpace: 349 | Description: 'Comments should start with a space.' 350 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space' 351 | Enabled: false 352 | 353 | Style/LineEndConcatenation: 354 | Description: >- 355 | Use \ instead of + or << to concatenate two string literals at 356 | line end. 357 | Enabled: false 358 | 359 | Style/MethodCallParentheses: 360 | Description: 'Do not use parentheses for method calls with no arguments.' 361 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens' 362 | Enabled: false 363 | 364 | Style/MethodDefParentheses: 365 | Description: >- 366 | Checks if the method definitions have or don't have 367 | parentheses. 368 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' 369 | Enabled: false 370 | 371 | Style/MethodName: 372 | Description: 'Use the configured style when naming methods.' 373 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars' 374 | Enabled: false 375 | 376 | Style/ModuleFunction: 377 | Description: 'Checks for usage of `extend self` in modules.' 378 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#module-function' 379 | Enabled: false 380 | 381 | Style/MultilineBlockChain: 382 | Description: 'Avoid multi-line chains of blocks.' 383 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' 384 | Enabled: false 385 | 386 | Style/MultilineBlockLayout: 387 | Description: 'Ensures newlines after multiline block do statements.' 388 | Enabled: false 389 | 390 | Style/MultilineIfThen: 391 | Description: 'Do not use then for multi-line if/unless.' 392 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-then' 393 | Enabled: false 394 | 395 | Style/MultilineOperationIndentation: 396 | Description: >- 397 | Checks indentation of binary operations that span more than 398 | one line. 399 | Enabled: false 400 | 401 | Style/MultilineTernaryOperator: 402 | Description: >- 403 | Avoid multi-line ?: (the ternary operator); 404 | use if/unless instead. 405 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-ternary' 406 | Enabled: false 407 | 408 | Style/NegatedIf: 409 | Description: >- 410 | Favor unless over if for negative conditions 411 | (or control flow or). 412 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#unless-for-negatives' 413 | Enabled: false 414 | 415 | Style/NegatedWhile: 416 | Description: 'Favor until over while for negative conditions.' 417 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#until-for-negatives' 418 | Enabled: false 419 | 420 | Style/NestedTernaryOperator: 421 | Description: 'Use one expression per branch in a ternary operator.' 422 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-ternary' 423 | Enabled: false 424 | 425 | Style/Next: 426 | Description: 'Use `next` to skip iteration instead of a condition at the end.' 427 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals' 428 | Enabled: false 429 | 430 | Style/NilComparison: 431 | Description: 'Prefer x.nil? to x == nil.' 432 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' 433 | Enabled: false 434 | 435 | Style/NonNilCheck: 436 | Description: 'Checks for redundant nil checks.' 437 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks' 438 | Enabled: false 439 | 440 | Style/Not: 441 | Description: 'Use ! instead of not.' 442 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not' 443 | Enabled: false 444 | 445 | Style/NumericLiterals: 446 | Description: >- 447 | Add underscores to large numeric literals to improve their 448 | readability. 449 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics' 450 | Enabled: false 451 | 452 | Style/OneLineConditional: 453 | Description: >- 454 | Favor the ternary operator(?:) over 455 | if/then/else/end constructs. 456 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator' 457 | Enabled: false 458 | 459 | Style/OpMethod: 460 | Description: 'When defining binary operators, name the argument other.' 461 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg' 462 | Enabled: false 463 | 464 | Style/OptionalArguments: 465 | Description: >- 466 | Checks for optional arguments that do not appear at the end 467 | of the argument list 468 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#optional-arguments' 469 | Enabled: false 470 | 471 | Style/ParallelAssignment: 472 | Description: >- 473 | Check for simple usages of parallel assignment. 474 | It will only warn when the number of variables 475 | matches on both sides of the assignment. 476 | This also provides performance benefits 477 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parallel-assignment' 478 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#parallel-assignment-vs-sequential-assignment-code' 479 | Enabled: false 480 | 481 | Style/ParenthesesAroundCondition: 482 | Description: >- 483 | Don't use parentheses around the condition of an 484 | if/unless/while. 485 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-parens-if' 486 | Enabled: false 487 | 488 | Style/PercentLiteralDelimiters: 489 | Description: 'Use `%`-literal delimiters consistently' 490 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-literal-braces' 491 | Enabled: false 492 | 493 | Style/PercentQLiterals: 494 | Description: 'Checks if uses of %Q/%q match the configured preference.' 495 | Enabled: false 496 | 497 | Style/PerlBackrefs: 498 | Description: 'Avoid Perl-style regex back references.' 499 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers' 500 | Enabled: false 501 | 502 | Style/PredicateName: 503 | Description: 'Check the names of predicate methods.' 504 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark' 505 | Enabled: false 506 | 507 | Style/Proc: 508 | Description: 'Use proc instead of Proc.new.' 509 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc' 510 | Enabled: false 511 | 512 | Style/RaiseArgs: 513 | Description: 'Checks the arguments passed to raise/fail.' 514 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#exception-class-messages' 515 | Enabled: false 516 | 517 | Style/RedundantBegin: 518 | Description: "Don't use begin blocks when they are not needed." 519 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#begin-implicit' 520 | Enabled: false 521 | 522 | Style/RedundantException: 523 | Description: "Checks for an obsolete RuntimeException argument in raise/fail." 524 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-runtimeerror' 525 | Enabled: false 526 | 527 | Style/RedundantReturn: 528 | Description: "Don't use return where it's not required." 529 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return' 530 | Enabled: false 531 | 532 | Style/RedundantSelf: 533 | Description: "Don't use self where it's not needed." 534 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-self-unless-required' 535 | Enabled: false 536 | 537 | Style/RegexpLiteral: 538 | Description: 'Use / or %r around regular expressions.' 539 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r' 540 | Enabled: false 541 | 542 | Style/RescueEnsureAlignment: 543 | Description: 'Align rescues and ensures correctly.' 544 | Enabled: false 545 | 546 | Style/RescueModifier: 547 | Description: 'Avoid using rescue in its modifier form.' 548 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-rescue-modifiers' 549 | Enabled: false 550 | 551 | Style/SelfAssignment: 552 | Description: >- 553 | Checks for places where self-assignment shorthand should have 554 | been used. 555 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#self-assignment' 556 | Enabled: false 557 | 558 | Style/Semicolon: 559 | Description: "Don't use semicolons to terminate expressions." 560 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon' 561 | Enabled: false 562 | 563 | Style/SignalException: 564 | Description: 'Checks for proper usage of fail and raise.' 565 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#fail-method' 566 | Enabled: false 567 | 568 | Style/SingleLineBlockParams: 569 | Description: 'Enforces the names of some block params.' 570 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks' 571 | Enabled: false 572 | 573 | Style/SingleLineMethods: 574 | Description: 'Avoid single-line methods.' 575 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-single-line-methods' 576 | Enabled: false 577 | 578 | Style/SingleSpaceBeforeFirstArg: 579 | Description: >- 580 | Checks that exactly one space is used between a method name 581 | and the first argument for method calls without parentheses. 582 | Enabled: false 583 | 584 | Style/SpaceAfterColon: 585 | Description: 'Use spaces after colons.' 586 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 587 | Enabled: false 588 | 589 | Style/SpaceAfterComma: 590 | Description: 'Use spaces after commas.' 591 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 592 | Enabled: false 593 | 594 | Style/SpaceAfterControlKeyword: 595 | Description: 'Use spaces after if/elsif/unless/while/until/case/when.' 596 | Enabled: false 597 | 598 | Style/SpaceAfterMethodName: 599 | Description: >- 600 | Do not put a space between a method name and the opening 601 | parenthesis in a method definition. 602 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' 603 | Enabled: false 604 | 605 | Style/SpaceAfterNot: 606 | Description: Tracks redundant space after the ! operator. 607 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-bang' 608 | Enabled: false 609 | 610 | Style/SpaceAfterSemicolon: 611 | Description: 'Use spaces after semicolons.' 612 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 613 | Enabled: false 614 | 615 | Style/SpaceBeforeBlockBraces: 616 | Description: >- 617 | Checks that the left block brace has or doesn't have space 618 | before it. 619 | Enabled: false 620 | 621 | Style/SpaceBeforeComma: 622 | Description: 'No spaces before commas.' 623 | Enabled: false 624 | 625 | Style/SpaceBeforeComment: 626 | Description: >- 627 | Checks for missing space between code and a comment on the 628 | same line. 629 | Enabled: false 630 | 631 | Style/SpaceBeforeSemicolon: 632 | Description: 'No spaces before semicolons.' 633 | Enabled: false 634 | 635 | Style/SpaceInsideBlockBraces: 636 | Description: >- 637 | Checks that block braces have or don't have surrounding space. 638 | For blocks taking parameters, checks that the left brace has 639 | or doesn't have trailing space. 640 | Enabled: false 641 | 642 | Style/SpaceAroundBlockParameters: 643 | Description: 'Checks the spacing inside and after block parameters pipes.' 644 | Enabled: false 645 | 646 | Style/SpaceAroundEqualsInParameterDefault: 647 | Description: >- 648 | Checks that the equals signs in parameter default assignments 649 | have or don't have surrounding space depending on 650 | configuration. 651 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-around-equals' 652 | Enabled: false 653 | 654 | Style/SpaceAroundOperators: 655 | Description: 'Use a single space around operators.' 656 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 657 | Enabled: false 658 | 659 | Style/SpaceBeforeModifierKeyword: 660 | Description: 'Put a space before the modifier keyword.' 661 | Enabled: false 662 | 663 | Style/SpaceInsideBrackets: 664 | Description: 'No spaces after [ or before ].' 665 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' 666 | Enabled: false 667 | 668 | Style/SpaceInsideHashLiteralBraces: 669 | Description: "Use spaces inside hash literal braces - or don't." 670 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 671 | Enabled: false 672 | 673 | Style/SpaceInsideParens: 674 | Description: 'No spaces after ( or before ).' 675 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' 676 | Enabled: false 677 | 678 | Style/SpaceInsideRangeLiteral: 679 | Description: 'No spaces inside range literals.' 680 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-inside-range-literals' 681 | Enabled: false 682 | 683 | Style/SpaceInsideStringInterpolation: 684 | Description: 'Checks for padding/surrounding spaces inside string interpolation.' 685 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#string-interpolation' 686 | Enabled: false 687 | 688 | Style/SpecialGlobalVars: 689 | Description: 'Avoid Perl-style global variables.' 690 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms' 691 | Enabled: false 692 | 693 | Style/StringLiterals: 694 | Description: 'Checks if uses of quotes match the configured preference.' 695 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals' 696 | Enabled: false 697 | 698 | Style/StringLiteralsInInterpolation: 699 | Description: >- 700 | Checks if uses of quotes inside expressions in interpolated 701 | strings match the configured preference. 702 | Enabled: false 703 | 704 | Style/StructInheritance: 705 | Description: 'Checks for inheritance from Struct.new.' 706 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-extend-struct-new' 707 | Enabled: false 708 | 709 | Style/SymbolLiteral: 710 | Description: 'Use plain symbols instead of string symbols when possible.' 711 | Enabled: false 712 | 713 | Style/SymbolProc: 714 | Description: 'Use symbols as procs instead of blocks when possible.' 715 | Enabled: false 716 | 717 | Style/Tab: 718 | Description: 'No hard tabs.' 719 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' 720 | Enabled: false 721 | 722 | Style/TrailingBlankLines: 723 | Description: 'Checks trailing blank lines and final newline.' 724 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof' 725 | Enabled: false 726 | 727 | Style/TrailingComma: 728 | Description: 'Checks for trailing comma in parameter lists and literals.' 729 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' 730 | Enabled: false 731 | 732 | Style/TrailingWhitespace: 733 | Description: 'Avoid trailing whitespace.' 734 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace' 735 | Enabled: false 736 | 737 | Style/TrivialAccessors: 738 | Description: 'Prefer attr_* methods to trivial readers/writers.' 739 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family' 740 | Enabled: false 741 | 742 | Style/UnlessElse: 743 | Description: >- 744 | Do not use unless with else. Rewrite these with the positive 745 | case first. 746 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless' 747 | Enabled: false 748 | 749 | Style/UnneededCapitalW: 750 | Description: 'Checks for %W when interpolation is not needed.' 751 | Enabled: false 752 | 753 | Style/UnneededPercentQ: 754 | Description: 'Checks for %q/%Q when single quotes or double quotes would do.' 755 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q' 756 | Enabled: false 757 | 758 | Style/TrailingUnderscoreVariable: 759 | Description: >- 760 | Checks for the usage of unneeded trailing underscores at the 761 | end of parallel variable assignment. 762 | Enabled: false 763 | 764 | Style/VariableInterpolation: 765 | Description: >- 766 | Don't interpolate global, instance and class variables 767 | directly in strings. 768 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#curlies-interpolate' 769 | Enabled: false 770 | 771 | Style/VariableName: 772 | Description: 'Use the configured style when naming variables.' 773 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars' 774 | Enabled: false 775 | 776 | Style/WhenThen: 777 | Description: 'Use when x then ... for one-line cases.' 778 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#one-line-cases' 779 | Enabled: false 780 | 781 | Style/WhileUntilDo: 782 | Description: 'Checks for redundant do after while or until.' 783 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-while-do' 784 | Enabled: false 785 | 786 | Style/WhileUntilModifier: 787 | Description: >- 788 | Favor modifier while/until usage when you have a 789 | single-line body. 790 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier' 791 | Enabled: false 792 | 793 | Style/WordArray: 794 | Description: 'Use %w or %W for arrays of words.' 795 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w' 796 | Enabled: false 797 | 798 | ########################################################## 799 | Metrics/AbcSize: 800 | Description: >- 801 | A calculated magnitude based on number of assignments, 802 | branches, and conditions. 803 | Reference: 'http://c2.com/cgi/wiki?AbcMetric' 804 | Enabled: true 805 | Max: 20 806 | 807 | Metrics/BlockNesting: 808 | Description: 'Avoid excessive block nesting' 809 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count' 810 | Enabled: true 811 | Max: 4 812 | 813 | Metrics/ClassLength: 814 | Description: 'Avoid classes longer than 100 lines of code.' 815 | Enabled: true 816 | Max: 150 817 | 818 | Metrics/ModuleLength: 819 | Description: 'Avoid modules longer than 100 lines of code.' 820 | Enabled: true 821 | Max: 150 822 | 823 | Metrics/CyclomaticComplexity: 824 | Description: >- 825 | A complexity metric that is strongly correlated to the number 826 | of test cases needed to validate a method. 827 | Enabled: false 828 | 829 | Metrics/LineLength: 830 | Description: 'Limit lines to 80 characters.' 831 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits' 832 | Enabled: false 833 | 834 | Metrics/MethodLength: 835 | Description: 'Avoid methods longer than 10 lines of code.' 836 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods' 837 | Enabled: false 838 | 839 | Metrics/ParameterLists: 840 | Description: 'Avoid parameter lists longer than three or four parameters.' 841 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params' 842 | Enabled: true 843 | 844 | Metrics/PerceivedComplexity: 845 | Description: >- 846 | A complexity metric geared towards measuring complexity for a 847 | human reader. 848 | Enabled: false 849 | 850 | #################### Lint ################################ 851 | ### Warnings 852 | 853 | Lint/AmbiguousOperator: 854 | Description: >- 855 | Checks for ambiguous operators in the first argument of a 856 | method invocation without parentheses. 857 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-as-args' 858 | Enabled: true 859 | 860 | Lint/AmbiguousRegexpLiteral: 861 | Description: >- 862 | Checks for ambiguous regexp literals in the first argument of 863 | a method invocation without parenthesis. 864 | Enabled: true 865 | 866 | Lint/AssignmentInCondition: 867 | Description: "Don't use assignment in conditions." 868 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition' 869 | Enabled: true 870 | 871 | Lint/BlockAlignment: 872 | Description: 'Align block ends correctly.' 873 | Enabled: true 874 | 875 | Lint/CircularArgumentReference: 876 | Description: "Don't refer to the keyword argument in the default value." 877 | Enabled: true 878 | 879 | Lint/ConditionPosition: 880 | Description: >- 881 | Checks for condition placed in a confusing position relative to 882 | the keyword. 883 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#same-line-condition' 884 | Enabled: true 885 | 886 | Lint/Debugger: 887 | Description: 'Check for debugger calls.' 888 | Enabled: true 889 | 890 | Lint/DefEndAlignment: 891 | Description: 'Align ends corresponding to defs correctly.' 892 | Enabled: true 893 | 894 | Lint/DeprecatedClassMethods: 895 | Description: 'Check for deprecated class method calls.' 896 | Enabled: true 897 | 898 | Lint/DuplicateMethods: 899 | Description: 'Check for duplicate methods calls.' 900 | Enabled: true 901 | 902 | Lint/EachWithObjectArgument: 903 | Description: 'Check for immutable argument given to each_with_object.' 904 | Enabled: true 905 | 906 | Lint/ElseLayout: 907 | Description: 'Check for odd code arrangement in an else block.' 908 | Enabled: true 909 | 910 | Lint/EmptyEnsure: 911 | Description: 'Checks for empty ensure block.' 912 | Enabled: true 913 | 914 | Lint/EmptyInterpolation: 915 | Description: 'Checks for empty string interpolation.' 916 | Enabled: true 917 | 918 | Lint/EndAlignment: 919 | Description: 'Align ends correctly.' 920 | Enabled: true 921 | 922 | Lint/EndInMethod: 923 | Description: 'END blocks should not be placed inside method definitions.' 924 | Enabled: true 925 | 926 | Lint/EnsureReturn: 927 | Description: 'Do not use return in an ensure block.' 928 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-return-ensure' 929 | Enabled: true 930 | 931 | Lint/Eval: 932 | Description: 'The use of eval represents a serious security risk.' 933 | Enabled: true 934 | 935 | Lint/FormatParameterMismatch: 936 | Description: 'The number of parameters to format/sprint must match the fields.' 937 | Enabled: true 938 | 939 | Lint/HandleExceptions: 940 | Description: "Don't suppress exception." 941 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions' 942 | Enabled: true 943 | 944 | Lint/InvalidCharacterLiteral: 945 | Description: >- 946 | Checks for invalid character literals with a non-escaped 947 | whitespace character. 948 | Enabled: true 949 | 950 | Lint/LiteralInCondition: 951 | Description: 'Checks of literals used in conditions.' 952 | Enabled: true 953 | 954 | Lint/LiteralInInterpolation: 955 | Description: 'Checks for literals used in interpolation.' 956 | Enabled: true 957 | 958 | Lint/Loop: 959 | Description: >- 960 | Use Kernel#loop with break rather than begin/end/until or 961 | begin/end/while for post-loop tests. 962 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#loop-with-break' 963 | Enabled: true 964 | 965 | Lint/NestedMethodDefinition: 966 | Description: 'Do not use nested method definitions.' 967 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-methods' 968 | Enabled: true 969 | 970 | Lint/NonLocalExitFromIterator: 971 | Description: 'Do not use return in iterator to cause non-local exit.' 972 | Enabled: true 973 | 974 | Lint/ParenthesesAsGroupedExpression: 975 | Description: >- 976 | Checks for method calls with a space before the opening 977 | parenthesis. 978 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' 979 | Enabled: true 980 | 981 | Lint/RequireParentheses: 982 | Description: >- 983 | Use parentheses in the method call to avoid confusion 984 | about precedence. 985 | Enabled: true 986 | 987 | Lint/RescueException: 988 | Description: 'Avoid rescuing the Exception class.' 989 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues' 990 | Enabled: true 991 | 992 | Lint/ShadowingOuterLocalVariable: 993 | Description: >- 994 | Do not use the same name as outer local variable 995 | for block arguments or block local variables. 996 | Enabled: true 997 | 998 | Lint/SpaceBeforeFirstArg: 999 | Description: >- 1000 | Put a space between a method name and the first argument 1001 | in a method call without parentheses. 1002 | Enabled: true 1003 | 1004 | Lint/StringConversionInInterpolation: 1005 | Description: 'Checks for Object#to_s usage in string interpolation.' 1006 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s' 1007 | Enabled: true 1008 | 1009 | Lint/UnderscorePrefixedVariableName: 1010 | Description: 'Do not use prefix `_` for a variable that is used.' 1011 | Enabled: true 1012 | 1013 | Lint/UnneededDisable: 1014 | Description: >- 1015 | Checks for rubocop:disable comments that can be removed. 1016 | Note: this cop is not disabled when disabling all cops. 1017 | It must be explicitly disabled. 1018 | Enabled: true 1019 | 1020 | Lint/UnusedBlockArgument: 1021 | Description: 'Checks for unused block arguments.' 1022 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' 1023 | Enabled: true 1024 | 1025 | Lint/UnusedMethodArgument: 1026 | Description: 'Checks for unused method arguments.' 1027 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' 1028 | Enabled: true 1029 | 1030 | Lint/UnreachableCode: 1031 | Description: 'Unreachable code.' 1032 | Enabled: true 1033 | 1034 | Lint/UselessAccessModifier: 1035 | Description: 'Checks for useless access modifiers.' 1036 | Enabled: true 1037 | 1038 | Lint/UselessAssignment: 1039 | Description: 'Checks for useless assignment to a local variable.' 1040 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' 1041 | Enabled: true 1042 | 1043 | Lint/UselessComparison: 1044 | Description: 'Checks for comparison of something with itself.' 1045 | Enabled: true 1046 | 1047 | Lint/UselessElseWithoutRescue: 1048 | Description: 'Checks for useless `else` in `begin..end` without `rescue`.' 1049 | Enabled: true 1050 | 1051 | Lint/UselessSetterCall: 1052 | Description: 'Checks for useless setter call to a local variable.' 1053 | Enabled: true 1054 | 1055 | Lint/Void: 1056 | Description: 'Possible use of operator/literal/variable in void context.' 1057 | Enabled: true 1058 | 1059 | ##################### Performance ############################# 1060 | 1061 | Performance/Count: 1062 | Description: >- 1063 | Use `count` instead of `select...size`, `reject...size`, 1064 | `select...count`, `reject...count`, `select...length`, 1065 | and `reject...length`. 1066 | Enabled: true 1067 | 1068 | Performance/Detect: 1069 | Description: >- 1070 | Use `detect` instead of `select.first`, `find_all.first`, 1071 | `select.last`, and `find_all.last`. 1072 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerabledetect-vs-enumerableselectfirst-code' 1073 | Enabled: true 1074 | 1075 | Performance/FlatMap: 1076 | Description: >- 1077 | Use `Enumerable#flat_map` 1078 | instead of `Enumerable#map...Array#flatten(1)` 1079 | or `Enumberable#collect..Array#flatten(1)` 1080 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablemaparrayflatten-vs-enumerableflat_map-code' 1081 | Enabled: true 1082 | EnabledForFlattenWithoutParams: false 1083 | # If enabled, this cop will warn about usages of 1084 | # `flatten` being called without any parameters. 1085 | # This can be dangerous since `flat_map` will only flatten 1 level, and 1086 | # `flatten` without any parameters can flatten multiple levels. 1087 | 1088 | Performance/ReverseEach: 1089 | Description: 'Use `reverse_each` instead of `reverse.each`.' 1090 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code' 1091 | Enabled: true 1092 | 1093 | Performance/Sample: 1094 | Description: >- 1095 | Use `sample` instead of `shuffle.first`, 1096 | `shuffle.last`, and `shuffle[Fixnum]`. 1097 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#arrayshufflefirst-vs-arraysample-code' 1098 | Enabled: true 1099 | 1100 | Performance/Size: 1101 | Description: >- 1102 | Use `size` instead of `count` for counting 1103 | the number of elements in `Array` and `Hash`. 1104 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#arraycount-vs-arraysize-code' 1105 | Enabled: true 1106 | 1107 | Performance/StringReplacement: 1108 | Description: >- 1109 | Use `tr` instead of `gsub` when you are replacing the same 1110 | number of characters. Use `delete` instead of `gsub` when 1111 | you are deleting characters. 1112 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#stringgsub-vs-stringtr-code' 1113 | Enabled: true 1114 | 1115 | ##################### Rails ################################## 1116 | # 1117 | # Consider enabling some/all of these if you use Rails. 1118 | # 1119 | Rails/ActionFilter: 1120 | Description: 'Enforces consistent use of action filter methods.' 1121 | Enabled: false 1122 | 1123 | Rails/Date: 1124 | Description: >- 1125 | Checks the correct usage of date aware methods, 1126 | such as Date.today, Date.current etc. 1127 | Enabled: false 1128 | 1129 | Rails/DefaultScope: 1130 | Description: 'Checks if the argument passed to default_scope is a block.' 1131 | Enabled: false 1132 | 1133 | Rails/Delegate: 1134 | Description: 'Prefer delegate method for delegations.' 1135 | Enabled: false 1136 | 1137 | Rails/FindBy: 1138 | Description: 'Prefer find_by over where.first.' 1139 | Enabled: false 1140 | 1141 | Rails/FindEach: 1142 | Description: 'Prefer all.find_each over all.find.' 1143 | Enabled: false 1144 | 1145 | Rails/HasAndBelongsToMany: 1146 | Description: 'Prefer has_many :through to has_and_belongs_to_many.' 1147 | Enabled: false 1148 | 1149 | Rails/Output: 1150 | Description: 'Checks for calls to puts, print, etc.' 1151 | Enabled: false 1152 | 1153 | Rails/ReadWriteAttribute: 1154 | Description: >- 1155 | Checks for read_attribute(:attr) and 1156 | write_attribute(:attr, val). 1157 | Enabled: false 1158 | 1159 | Rails/ScopeArgs: 1160 | Description: 'Checks the arguments of ActiveRecord scopes.' 1161 | Enabled: false 1162 | 1163 | Rails/TimeZone: 1164 | Description: 'Checks the correct usage of time zone aware methods.' 1165 | StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time' 1166 | Reference: 'http://danilenko.org/2012/7/6/rails_timezones' 1167 | Enabled: false 1168 | 1169 | Rails/Validation: 1170 | Description: 'Use validates :attribute, hash of validations.' 1171 | Enabled: false 1172 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM fpco/stack-build-small:lts-13.17 as builder 3 | MAINTAINER Pat Brisbin 4 | ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 LC_ALL=C.UTF-8 5 | RUN \ 6 | apt-get update && \ 7 | apt-get install -y --no-install-recommends \ 8 | libpq-dev \ 9 | locales && \ 10 | locale-gen en_US.UTF-8 && \ 11 | rm -rf /var/lib/apt/lists/* 12 | 13 | ENV PATH /root/.local/bin:$PATH 14 | 15 | RUN mkdir -p /src 16 | WORKDIR /src 17 | 18 | COPY stack.yaml /src/ 19 | RUN stack setup 20 | 21 | COPY tee-io.cabal /src/tee-io.cabal 22 | RUN stack install --dependencies-only 23 | 24 | COPY src /src/src 25 | COPY app /src/app 26 | COPY config /src/config 27 | COPY static /src/static 28 | COPY templates /src/templates 29 | RUN stack install 30 | 31 | # Runtime 32 | FROM ubuntu:18.04 33 | MAINTAINER Pat Brisbin 34 | ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 LC_ALL=C.UTF-8 35 | RUN \ 36 | apt-get update && \ 37 | apt-get install -y --no-install-recommends \ 38 | ca-certificates \ 39 | gcc \ 40 | libpq-dev \ 41 | locales \ 42 | netbase && \ 43 | locale-gen en_US.UTF-8 && \ 44 | rm -rf /var/lib/apt/lists/* 45 | 46 | RUN mkdir -p /app 47 | WORKDIR /app 48 | COPY config /app/config 49 | COPY static /app/static 50 | COPY --from=builder /root/.local/bin/tee-io /app/tee-io 51 | COPY --from=builder /root/.local/bin/tee-io-worker /app/tee-io-worker 52 | COPY --from=builder /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 53 | 54 | CMD ["/app/tee-io"] 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Pat Brisbin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: setup-db setup-app test build binaries production release repl 2 | 3 | setup-db: 4 | docker-compose exec --user postgres postgres sh -c \ 5 | "createdb teeio && createdb teeio_test" 6 | 7 | setup-app: 8 | stack setup 9 | stack build --fast 10 | stack build --fast --test --no-run-tests 11 | 12 | setup-ci: 13 | sed -i 's/postgres:postgres@//' .env.test 14 | mkdir -p docker/stack docker/stack-work 15 | sudo chown root:root docker/stack docker/stack-work 16 | createdb teeio_test 17 | docker run --detach --publish 4569:4569 lphoward/fake-s3 18 | 19 | test: 20 | stack test 21 | 22 | repl: 23 | stack repl --ghci-options="-DDEVELOPMENT -O0 -fobject-code" 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tee.io 2 | 3 | It's like `tee(1)` as a service. 4 | 5 | For details, see the [home page](https://tee-io.onrender.com/) 6 | 7 | ## Development & Test 8 | 9 | 1. Start the backing services (PostgreSQL & Fake-S3) 10 | 11 | ``` 12 | docker-compose up -d 13 | ``` 14 | 15 | 1. Set up stack 16 | 17 | ``` 18 | make setup-app 19 | ``` 20 | 21 | 1. Create development and test databases 22 | 23 | ``` 24 | make setup-db 25 | ``` 26 | 27 | 1. Develop normally 28 | 29 | ``` 30 | stack test 31 | stack repl --ghci-options="-DDEVELOPMENT -O0 -fobject-code" 32 | ``` 33 | -------------------------------------------------------------------------------- /app/DevelMain.hs: -------------------------------------------------------------------------------- 1 | module DevelMain where 2 | 3 | import Prelude 4 | import Application (getApplicationRepl, shutdownApp) 5 | 6 | import Control.Exception (finally) 7 | import Control.Monad ((>=>)) 8 | import Control.Concurrent 9 | import Data.IORef 10 | import Foreign.Store 11 | import Network.Wai.Handler.Warp 12 | import GHC.Word 13 | 14 | update :: IO () 15 | update = do 16 | mtidStore <- lookupStore tidStoreNum 17 | case mtidStore of 18 | -- no server running 19 | Nothing -> do 20 | done <- storeAction doneStore newEmptyMVar 21 | tid <- start done 22 | _ <- storeAction (Store tidStoreNum) (newIORef tid) 23 | return () 24 | -- server is already running 25 | Just tidStore -> restartAppInNewThread tidStore 26 | where 27 | doneStore :: Store (MVar ()) 28 | doneStore = Store 0 29 | 30 | restartAppInNewThread :: Store (IORef ThreadId) -> IO () 31 | restartAppInNewThread tidStore = modifyStoredIORef tidStore $ \tid -> do 32 | killThread tid 33 | withStore doneStore takeMVar 34 | readStore doneStore >>= start 35 | 36 | 37 | start :: MVar () -> IO ThreadId 38 | start done = do 39 | (port, site, app) <- getApplicationRepl 40 | forkIO (finally (runSettings (setPort port defaultSettings) app) 41 | -- Note that this implies concurrency 42 | -- between shutdownApp and the next app that is starting. 43 | -- Normally this should be fine 44 | (putMVar done () >> shutdownApp site)) 45 | 46 | shutdown :: IO () 47 | shutdown = do 48 | mtidStore <- lookupStore tidStoreNum 49 | case mtidStore of 50 | Nothing -> putStrLn "no Yesod app running" 51 | Just tidStore -> do 52 | withStore tidStore $ readIORef >=> killThread 53 | putStrLn "Yesod app is shutdown" 54 | 55 | tidStoreNum :: Word32 56 | tidStoreNum = 1 57 | 58 | modifyStoredIORef :: Store (IORef a) -> (a -> IO a) -> IO () 59 | modifyStoredIORef store f = withStore store $ \ref -> do 60 | v <- readIORef ref 61 | f v >>= writeIORef ref 62 | -------------------------------------------------------------------------------- /app/devel.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE PackageImports #-} 2 | import "tee-io" Application (develMain) 3 | import Prelude (IO) 4 | 5 | main :: IO () 6 | main = develMain 7 | -------------------------------------------------------------------------------- /app/main-worker.hs: -------------------------------------------------------------------------------- 1 | import Prelude (IO) 2 | import Worker (workerMain) 3 | 4 | main :: IO () 5 | main = workerMain 6 | -------------------------------------------------------------------------------- /app/main.hs: -------------------------------------------------------------------------------- 1 | import Prelude (IO) 2 | import Application (appMain) 3 | 4 | main :: IO () 5 | main = appMain 6 | -------------------------------------------------------------------------------- /bin/example: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | { 3 | 4 | echo "hello"; sleep 3 5 | 6 | echo "i am an example command printing some output"; sleep 1 7 | 8 | echo "you can imagine i'm running a test" 9 | echo "or deploying an application" 10 | echo "or maybe reading some log files"; sleep 2 11 | 12 | echo "my output is being forwarded to tee.io via this script:" 13 | echo ""; sleep 2 14 | sed 's/^/ > /g' static/tee-io 15 | echo ""; sleep 1 16 | 17 | echo "you should be seeing it live in a web page" 18 | echo "as it's produced"; sleep 1 19 | echo "hopefully it worked?"; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | pre: 3 | # https://github.com/commercialhaskell/stack/issues/1658 4 | - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.6 20 5 | - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.6 20 6 | - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.9 10 7 | - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.9 10 8 | services: 9 | - docker 10 | - postgresql 11 | 12 | dependencies: 13 | cache_directories: 14 | - "~/.stack" 15 | - ".stack-work" 16 | - "docker/stack" 17 | - "docker/stack-work" 18 | pre: 19 | - wget https://github.com/commercialhaskell/stack/releases/download/v1.3.2/stack-1.3.2-linux-x86_64.tar.gz -O /tmp/stack.tar.gz 20 | - tar xvzOf /tmp/stack.tar.gz stack-1.3.2-linux-x86_64/stack > /tmp/stack 21 | - chmod +x /tmp/stack && sudo mv /tmp/stack /usr/bin/stack 22 | override: 23 | - make setup-app 24 | - make setup-ci 25 | 26 | test: 27 | override: 28 | - make test 29 | - make build 30 | - make binaries 31 | -------------------------------------------------------------------------------- /config/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbrisbin/tee-io/7f914cf7c40a4a9331c61feb8cd432b61ae5eb4e/config/favicon.ico -------------------------------------------------------------------------------- /config/keter.yml: -------------------------------------------------------------------------------- 1 | # After you've edited this file, remove the following line to allow 2 | # `yesod keter` to build your bundle. 3 | user-edited: false 4 | 5 | # A Keter app is composed of 1 or more stanzas. The main stanza will define our 6 | # web application. See the Keter documentation for more information on 7 | # available stanzas. 8 | stanzas: 9 | 10 | # Your Yesod application. 11 | - type: webapp 12 | 13 | # Name of your executable. You are unlikely to need to change this. 14 | # Note that all file paths are relative to the keter.yml file. 15 | exec: ../dist/build/tee-io/tee-io 16 | 17 | # Command line options passed to your application. 18 | args: [] 19 | 20 | hosts: 21 | # You can specify one or more hostnames for your application to respond 22 | # to. The primary hostname will be used for generating your application 23 | # root. 24 | - www.tee-io.com 25 | 26 | # Enable to force Keter to redirect to https 27 | # Can be added to any stanza 28 | requires-secure: false 29 | 30 | # Static files. 31 | - type: static-files 32 | hosts: 33 | - static.tee-io.com 34 | root: ../static 35 | 36 | # Uncomment to turn on directory listings. 37 | # directory-listing: true 38 | 39 | # Redirect plain domain name to www. 40 | - type: redirect 41 | 42 | hosts: 43 | - tee-io.com 44 | actions: 45 | - host: www.tee-io.com 46 | # secure: false 47 | # port: 80 48 | 49 | # Uncomment to switch to a non-permanent redirect. 50 | # status: 303 51 | 52 | # Use the following to automatically copy your bundle upon creation via `yesod 53 | # keter`. Uses `scp` internally, so you can set it to a remote destination 54 | # copy-to: user@host:/opt/keter/incoming/ 55 | 56 | # You can pass arguments to `scp` used above. This example limits bandwidth to 57 | # 1024 Kbit/s and uses port 2222 instead of the default 22 58 | # copy-to-args: 59 | # - "-l 1024" 60 | # - "-P 2222" 61 | 62 | # If you would like to have Keter automatically create a PostgreSQL database 63 | # and set appropriate environment variables for it to be discovered, uncomment 64 | # the following line. 65 | # plugins: 66 | # postgres: true 67 | -------------------------------------------------------------------------------- /config/models: -------------------------------------------------------------------------------- 1 | Command 2 | token Token 3 | running Bool SafeToRemove 4 | description Text Maybe 5 | createdAt UTCTime 6 | UniqueCommand token 7 | deriving Eq Show 8 | 9 | Output 10 | command CommandId 11 | content Text 12 | createdAt UTCTime 13 | -------------------------------------------------------------------------------- /config/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /config/routes: -------------------------------------------------------------------------------- 1 | /static StaticR Static appStatic 2 | 3 | /favicon.ico FaviconR GET 4 | /robots.txt RobotsR GET 5 | 6 | / HomeR GET 7 | /commands CommandsR POST 8 | /commands/#Token CommandR GET PATCH PUT DELETE 9 | /commands/#Token/output OutputR GET POST 10 | -------------------------------------------------------------------------------- /config/settings.yml: -------------------------------------------------------------------------------- 1 | approot: "_env:APPROOT:http://localhost:3000" 2 | command-timeout: "_env:COMMAND_TIMEOUT:300" 3 | database-pool-size: "_env:PGPOOLSIZE:10" # use conventional variable 4 | database-url: "_env:DATABASE_URL:postgres://postgres:postgres@localhost:5432/teeio" 5 | host: "_env:HOST:*4" # any IPv4 host 6 | ip-from-header: "_env:IP_FROM_HEADER:false" 7 | log-level: "_env:LOG_LEVEL:info" 8 | mutable-static: "_env:MUTABLE_STATIC:false" 9 | port: "_env:PORT:3000" 10 | s3-url: "_env:S3_URL:https://s3.amazonaws.com/tee.io.development" 11 | -------------------------------------------------------------------------------- /config/sqlite.yml: -------------------------------------------------------------------------------- 1 | Default: &defaults 2 | database: tee-io.sqlite3 3 | poolsize: 10 4 | 5 | Development: 6 | <<: *defaults 7 | 8 | Testing: 9 | database: tee-io_test.sqlite3 10 | <<: *defaults 11 | 12 | Staging: 13 | database: tee-io_staging.sqlite3 14 | poolsize: 100 15 | <<: *defaults 16 | 17 | Production: 18 | database: tee-io_production.sqlite3 19 | poolsize: 100 20 | <<: *defaults 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | volumes: 4 | postgresql: 5 | fake-s3: 6 | 7 | services: 8 | postgres: 9 | image: postgres 10 | ports: 11 | - 5432:5432 12 | volumes: 13 | - postgresql:/var/lib/postgresql/data 14 | 15 | fake-s3: 16 | image: lphoward/fake-s3 17 | ports: 18 | - 4569:4569 19 | volumes: 20 | - fake-s3:/fakes3_root 21 | -------------------------------------------------------------------------------- /render.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - type: web 3 | plan: free 4 | name: tee-io 5 | env: docker 6 | envVars: 7 | - fromGroup: vars 8 | - key: DATABASE_URL 9 | fromDatabase: 10 | name: tee-io 11 | property: connectionString 12 | 13 | # - type: cron 14 | # name: tee-io-worker 15 | # env: docker 16 | # envVars: 17 | # - fromGroup: vars 18 | # - key: DATABASE_URL 19 | # fromDatabase: 20 | # name: tee-io 21 | # property: connectionString 22 | 23 | # schedule: "*/10 * * * *" 24 | # dockerCommand: /app/tee-io-worker 25 | 26 | databases: 27 | - name: tee-io 28 | plan: free 29 | ipAllowList: [] 30 | 31 | envVarGroups: 32 | - name: vars 33 | envVars: 34 | - key: APPROOT 35 | value: https://tee-io.onrender.com 36 | - key: S3_BUCKET 37 | value: tee.io 38 | - key: S3_URL 39 | value: https://s3.amazonaws.com/tee.io 40 | -------------------------------------------------------------------------------- /sdk/ruby/.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.2.2 2 | -------------------------------------------------------------------------------- /sdk/ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /sdk/ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | tee-io (0.0.1) 5 | curb (~> 0.8.8) 6 | posix-spawn (~> 0.3.11) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | addressable (2.3.8) 12 | crack (0.4.2) 13 | safe_yaml (~> 1.0.0) 14 | curb (0.8.8) 15 | diff-lcs (1.2.5) 16 | posix-spawn (0.3.11) 17 | rake (13.0.1) 18 | rspec (3.3.0) 19 | rspec-core (~> 3.3.0) 20 | rspec-expectations (~> 3.3.0) 21 | rspec-mocks (~> 3.3.0) 22 | rspec-core (3.3.2) 23 | rspec-support (~> 3.3.0) 24 | rspec-expectations (3.3.1) 25 | diff-lcs (>= 1.2.0, < 2.0) 26 | rspec-support (~> 3.3.0) 27 | rspec-mocks (3.3.2) 28 | diff-lcs (>= 1.2.0, < 2.0) 29 | rspec-support (~> 3.3.0) 30 | rspec-support (3.3.0) 31 | safe_yaml (1.0.4) 32 | webmock (1.21.0) 33 | addressable (>= 2.3.6) 34 | crack (>= 0.3.2) 35 | 36 | PLATFORMS 37 | ruby 38 | 39 | DEPENDENCIES 40 | rake 41 | rspec 42 | tee-io! 43 | webmock 44 | 45 | BUNDLED WITH 46 | 1.10.6 47 | -------------------------------------------------------------------------------- /sdk/ruby/README.md: -------------------------------------------------------------------------------- 1 | # tee.io Ruby SDK 2 | 3 | Ruby client SDK for [tee.io][]. 4 | 5 | [tee.io]: https://tee-io.onrender.com 6 | 7 | ## Installation 8 | 9 | ``` 10 | gem "tee-io", require: "tee_io" 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```rb 16 | TeeIO.run("cap deploy") { |url| puts url } 17 | ``` 18 | 19 | The following keyword options are supported: 20 | 21 | - `description`: supply a description, defaults to the command 22 | - `timeout`: timeout in seconds, defaults to `5 * 60` 23 | - `base_url`: override the base URL (useful for local testing) 24 | -------------------------------------------------------------------------------- /sdk/ruby/Rakefile: -------------------------------------------------------------------------------- 1 | require "rspec/core/rake_task" 2 | require "bundler/gem_tasks" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /sdk/ruby/VERSION: -------------------------------------------------------------------------------- 1 | 0.0.1 2 | -------------------------------------------------------------------------------- /sdk/ruby/example.rb: -------------------------------------------------------------------------------- 1 | require "tee_io" 2 | 3 | TeeIO.run("../../bin/example", &method(:puts)) 4 | -------------------------------------------------------------------------------- /sdk/ruby/lib/tee_io.rb: -------------------------------------------------------------------------------- 1 | require "curb" 2 | require "json" 3 | require "posix/spawn" 4 | require "timeout" 5 | 6 | require "tee_io/api" 7 | require "tee_io/process" 8 | require "tee_io/token_response" 9 | 10 | module TeeIO 11 | DEFAULT_URL = "https://tee-io.onrender.com" 12 | DEFAULT_TIMEOUT = 5 * 60 13 | 14 | def self.run(*command, description: nil, base_url: DEFAULT_URL, timeout: DEFAULT_TIMEOUT) 15 | api = API.new(base_url) 16 | resp = api.create_command(description || "#{command.join(" ")}") 17 | token = TokenResponse.new(resp).token 18 | 19 | yield "#{base_url}/commands/#{token}" 20 | 21 | process = Process.new(command, timeout) 22 | process.run do |_stream, io| 23 | io.each_line do |line| 24 | api.create_output(token, line) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /sdk/ruby/lib/tee_io/api.rb: -------------------------------------------------------------------------------- 1 | require "curb" 2 | 3 | module TeeIO 4 | class API 5 | def initialize(base_url) 6 | @base_url = base_url 7 | end 8 | 9 | def create_command(description) 10 | request(:post, "/commands", description: description) 11 | end 12 | 13 | def create_output(token, content) 14 | request(:post, "/commands/#{token}/output", content: content) 15 | end 16 | 17 | private 18 | 19 | attr_reader :base_url 20 | 21 | def request(method, path, body) 22 | Curl.send(method, "#{base_url}#{path}", body.to_json) do |curl| 23 | curl.headers["Accept"] = "application/json" 24 | curl.headers["Content-Type"] = "application/json" 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /sdk/ruby/lib/tee_io/process.rb: -------------------------------------------------------------------------------- 1 | module TeeIO 2 | class Process 3 | def initialize(command, timeout) 4 | @command = command 5 | @timeout = timeout 6 | end 7 | 8 | def run(&block) 9 | pid, stdin, out, err = POSIX::Spawn.popen4(*command) 10 | 11 | stdin.close 12 | stdout_thread = Thread.new { block.call(:stdout, out) } 13 | stderr_thread = Thread.new { block.call(:stderr, err) } 14 | 15 | status = Timeout.timeout(timeout) do 16 | ::Process.waitpid2(pid)[1] 17 | end 18 | 19 | stdout_thread.join 20 | stderr_thread.join 21 | 22 | status 23 | rescue Exception => ex 24 | if pid 25 | begin 26 | ::Process.kill("KILL", pid) 27 | ::Process.waitpid2(pid) 28 | rescue Errno::ESRCH 29 | end 30 | end 31 | 32 | stdout_thread.kill if stdout_thread 33 | stderr_thread.kill if stderr_thread 34 | 35 | raise 36 | end 37 | 38 | private 39 | 40 | attr_reader :command, :arguments, :timeout 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /sdk/ruby/lib/tee_io/token_response.rb: -------------------------------------------------------------------------------- 1 | module TeeIO 2 | class TokenResponse 3 | InvalidResponseError = Class.new(StandardError) 4 | 5 | def initialize(response) 6 | code = response.response_code 7 | 8 | unless [200, 201].include?(code) 9 | raise_invalid!("#{code} #{response.body_str}") 10 | end 11 | 12 | @response = response 13 | end 14 | 15 | def token 16 | json_response["token"] or raise_invalid!("no token present") 17 | end 18 | 19 | private 20 | 21 | attr_reader :response 22 | 23 | def json_response 24 | JSON.parse(response.body_str) 25 | rescue JSON::ParserError => ex 26 | raise_invalid!("JSON parse error: #{ex.message}") 27 | end 28 | 29 | def raise_invalid!(message) 30 | raise InvalidResponseError, 31 | "Unable to start command #{message}" 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /sdk/ruby/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "tee_io" 2 | require "webmock/rspec" 3 | -------------------------------------------------------------------------------- /sdk/ruby/spec/tee_io/api_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module TeeIO 4 | describe API do 5 | let(:api) { API.new(base_url) } 6 | let(:base_url) { "http://localhost:3000" } 7 | 8 | describe "#create_command" do 9 | it "POSTs to /commands with the given description" do 10 | req = stub_request(:post, "#{base_url}/commands"). 11 | with(body: "{\"description\":\"description\"}") 12 | 13 | api.create_command("description") 14 | 15 | expect(req).to have_been_requested 16 | end 17 | end 18 | 19 | describe "#create_output" do 20 | it "POSTs to /commands/:token/output with the given content" do 21 | req = stub_request(:post, "#{base_url}/commands/token/output"). 22 | with(body: "{\"content\":\"content\"}") 23 | 24 | api.create_output("token", "content") 25 | 26 | expect(req).to have_been_requested 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /sdk/ruby/spec/tee_io/process_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module TeeIO 4 | describe Process do 5 | it "runs a process and streams output" do 6 | stdout = [] 7 | stderr = [] 8 | process = Process.new( 9 | ["sh", "-c", "echo stdout; echo stderr >&2"], 10 10 | ) 11 | 12 | status = process.run do |stream, io| 13 | case stream 14 | when :stdout then stdout += io.each_line.to_a 15 | when :stderr then stderr += io.each_line.to_a 16 | end 17 | end 18 | 19 | expect(status).to be_success 20 | expect(stdout).to eq ["stdout\n"] 21 | expect(stderr).to eq ["stderr\n"] 22 | end 23 | 24 | it "wraps the process in a timeout" do 25 | process = Process.new(["sleep", "10"], 0.1) 26 | 27 | expect { process.run }.to raise_error(Timeout::Error) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /sdk/ruby/spec/tee_io/token_response_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module TeeIO 4 | describe TokenResponse do 5 | describe "#token" do 6 | it "returns the token" do 7 | response = http_success('{"token":"abc123"}') 8 | 9 | token = TokenResponse.new(response).token 10 | 11 | expect(token).to eq "abc123" 12 | end 13 | 14 | it "raises on unexpected JSON (no token)" do 15 | response = http_success('{"not_token":true}') 16 | 17 | expect_invalid_response(response, /no token/) 18 | end 19 | 20 | it "raises on unparsable JSON" do 21 | response = http_success("{invalid json") 22 | 23 | expect_invalid_response(response, /unexpected token/) 24 | end 25 | 26 | it "raises on unsuccessful HTTP responses" do 27 | response = double(response_code: 400, body_str: "bad method") 28 | 29 | expect_invalid_response(response, /400 bad method/) 30 | end 31 | end 32 | 33 | def http_success(body) 34 | double(response_code: 200, body_str: body) 35 | end 36 | 37 | def expect_invalid_response(response, message) 38 | expect { TokenResponse.new(response).token }.to raise_error( 39 | TokenResponse::InvalidResponseError, message 40 | ) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /sdk/ruby/spec/tee_io_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe TeeIO do 4 | describe ".run" do 5 | let(:token) { "abc123" } 6 | let(:base_url) { "http://localhost:3000" } 7 | 8 | it "runs the command, streams to tee-io, and yields the URL" do 9 | requests = [ 10 | r(:post, "/commands", "{\"description\":\"test command\"}", "{\"token\":\"#{token}\"}"), 11 | r(:post, "/commands/#{token}/output", "{\"content\":\"foo\\n\"}"), 12 | r(:post, "/commands/#{token}/output", "{\"content\":\"bar\\n\"}"), 13 | r(:post, "/commands/#{token}/output", "{\"content\":\"baz\\n\"}"), 14 | ] 15 | 16 | args = { 17 | } 18 | 19 | status = TeeIO.run( 20 | "sh", "-c", "echo foo; echo bar; echo baz >&2; false", 21 | description: "test command", 22 | base_url: base_url, 23 | ) do |url| 24 | expect(url).to eq "#{base_url}/commands/#{token}" 25 | end 26 | 27 | expect(status).not_to be_success 28 | requests.each { |req| expect(req).to have_been_made } 29 | end 30 | 31 | def r(method, path, request, response = "{}") 32 | stub_request(method, "#{base_url}#{path}"). 33 | with(body: request).to_return(status: 200, body: response) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /sdk/ruby/tee-io.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(__FILE__, "../lib")) 2 | VERSION = File.read(File.expand_path("../VERSION", __FILE__)) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "tee-io" 6 | s.version = VERSION 7 | s.summary = "tee.io client SDK" 8 | s.license = "MIT" 9 | s.authors = "Pat Brisbin" 10 | s.email = "pbrisbin@gmail.com" 11 | s.homepage = "https://tee-io.onrender.com" 12 | s.description = "Client SDK for interfacing with tee.io" 13 | 14 | s.files = Dir["lib/**/*.rb"] 15 | s.require_paths = ["lib"] 16 | 17 | s.add_dependency "curb", "~>0.8.8" 18 | s.add_dependency "posix-spawn", "~>0.3.11" 19 | 20 | s.add_development_dependency "rake" 21 | s.add_development_dependency "rspec" 22 | s.add_development_dependency "webmock" 23 | end 24 | -------------------------------------------------------------------------------- /src/Application.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -fno-warn-orphans #-} 2 | module Application 3 | ( appMain 4 | , develMain 5 | , makeFoundation 6 | , makeLogWare 7 | , getApplicationRepl 8 | , shutdownApp 9 | , handler 10 | , db 11 | ) where 12 | 13 | import Import 14 | 15 | import Handler.Common 16 | import Handler.Home 17 | import Handler.Command 18 | import Handler.Output 19 | 20 | import Control.Lens (set) 21 | import Control.Monad.Logger (liftLoc, runLoggingT) 22 | import Database.Persist.Postgresql 23 | ( createPostgresqlPool 24 | , pgConnStr 25 | , pgPoolSize 26 | , runSqlPool 27 | ) 28 | import Language.Haskell.TH.Syntax (qLocation) 29 | import LoadEnv (loadEnv) 30 | import Network.Wai (Middleware) 31 | import Network.Wai.Handler.Warp 32 | ( Settings 33 | , defaultSettings 34 | , defaultShouldDisplayException 35 | , runSettings 36 | , setHost 37 | , setOnException 38 | , setPort 39 | , getPort 40 | ) 41 | import Network.Wai.Middleware.RequestLogger 42 | ( Destination(Callback, Logger) 43 | , IPAddrSource(..) 44 | , OutputFormat(..) 45 | , destination 46 | , mkRequestLogger 47 | , outputFormat 48 | ) 49 | import System.Log.FastLogger 50 | ( defaultBufSize 51 | , newStdoutLoggerSet 52 | , toLogStr 53 | ) 54 | 55 | import qualified Data.Text as T 56 | import qualified Network.AWS as AWS 57 | 58 | mkYesodDispatch "App" resourcesApp 59 | 60 | makeFoundation :: AppSettings -> IO App 61 | makeFoundation appSettings = do 62 | appAWSEnv <- newAWSEnv $ appSettings `allowsLevel` LevelDebug 63 | appHttpManager <- newManager 64 | appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger 65 | appStatic <- 66 | (if appMutableStatic appSettings then staticDevel else static) 67 | (appStaticDir appSettings) 68 | 69 | -- Bootstrap a log function to use when creating the connection pool 70 | let mkFoundation appConnPool = App{..} 71 | tempFoundation = mkFoundation $ error "connPool forced in tempFoundation" 72 | logFunc = messageLoggerSource tempFoundation appLogger 73 | 74 | pool <- flip runLoggingT logFunc $ createPostgresqlPool 75 | (pgConnStr $ appDatabaseConf appSettings) 76 | (pgPoolSize $ appDatabaseConf appSettings) 77 | 78 | -- Perform database migrations 79 | runLoggingT (runSqlPool (runMigration migrateAll) pool) logFunc 80 | 81 | -- Output settings at startup 82 | runLoggingT ($(logInfo) $ "settings " <> T.pack (show appSettings)) logFunc 83 | 84 | return $ mkFoundation pool 85 | 86 | where 87 | newAWSEnv debug = AWS.configure (appS3Service appSettings) <$> do 88 | logger <- AWS.newLogger (if debug then AWS.Debug else AWS.Error) stdout 89 | set AWS.envLogger logger <$> AWS.newEnv AWS.Discover 90 | 91 | makeApplication :: App -> IO Application 92 | makeApplication foundation = do 93 | logWare <- makeLogWare foundation 94 | appPlain <- toWaiAppPlain foundation 95 | return $ logWare $ defaultMiddlewaresNoLogging appPlain 96 | 97 | makeLogWare :: App -> IO Middleware 98 | makeLogWare foundation = mkRequestLogger def 99 | { outputFormat = if appSettings foundation `allowsLevel` LevelDebug 100 | then Detailed True 101 | else Apache apacheIpSource 102 | , destination = if appSettings foundation `allowsLevel` LevelInfo 103 | then Logger $ loggerSet $ appLogger foundation 104 | else Callback $ \_ -> return () 105 | } 106 | where 107 | apacheIpSource = if appIpFromHeader $ appSettings foundation 108 | then FromFallback 109 | else FromSocket 110 | 111 | warpSettings :: App -> Settings 112 | warpSettings foundation = 113 | setPort (appPort $ appSettings foundation) $ 114 | setHost (appHost $ appSettings foundation) $ 115 | setOnException (\_req e -> 116 | when (defaultShouldDisplayException e) $ messageLoggerSource 117 | foundation 118 | (appLogger foundation) 119 | $(qLocation >>= liftLoc) 120 | "yesod" 121 | LevelError 122 | (toLogStr $ "Exception from Warp: " ++ show e)) 123 | defaultSettings 124 | 125 | getAppSettings :: IO AppSettings 126 | getAppSettings = do 127 | loadEnv 128 | loadYamlSettings [configSettingsYml] [] useEnv 129 | 130 | develMain :: IO () 131 | develMain = develMainHelper $ do 132 | settings <- getAppSettings 133 | foundation <- makeFoundation settings 134 | wsettings <- getDevSettings $ warpSettings foundation 135 | app <- makeApplication foundation 136 | return (wsettings, app) 137 | 138 | appMain :: IO () 139 | appMain = do 140 | settings <- getAppSettings 141 | foundation <- makeFoundation settings 142 | app <- makeApplication foundation 143 | 144 | runSettings (warpSettings foundation) app 145 | 146 | -------------------------------------------------------------------------------- 147 | -- Functions for the REPL 148 | -------------------------------------------------------------------------------- 149 | 150 | getApplicationRepl :: IO (Int, App, Application) 151 | getApplicationRepl = do 152 | settings <- getAppSettings 153 | foundation <- makeFoundation settings 154 | wsettings <- getDevSettings $ warpSettings foundation 155 | app1 <- makeApplication foundation 156 | return (getPort wsettings, foundation, app1) 157 | 158 | shutdownApp :: App -> IO () 159 | shutdownApp _ = return () 160 | 161 | handler :: Handler a -> IO a 162 | handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h 163 | 164 | db :: ReaderT SqlBackend Handler a -> IO a 165 | db = handler . runDB 166 | -------------------------------------------------------------------------------- /src/Archive.hs: -------------------------------------------------------------------------------- 1 | module Archive 2 | ( archiveOutput 3 | , archivedOutput 4 | , deleteArchivedOutput 5 | ) where 6 | 7 | import Import 8 | 9 | import Control.Lens 10 | import Data.Conduit.Binary (sinkLbs) 11 | import Network.AWS 12 | import Network.AWS.S3 13 | 14 | import qualified Data.ByteString.Lazy as BL 15 | 16 | archiveOutput :: Token -> [Output] -> Handler () 17 | archiveOutput token outputs = runS3 token $ \b k -> 18 | void $ send $ putObject b k $ toBody $ 19 | BL.fromStrict $ concatMap (encodeUtf8 . outputContent) outputs 20 | 21 | archivedOutput :: Token -> Handler BL.ByteString 22 | archivedOutput token = runS3 token $ \b k -> do 23 | rs <- send $ getObject b k 24 | view gorsBody rs `sinkBody` sinkLbs 25 | 26 | deleteArchivedOutput :: Token -> Handler () 27 | deleteArchivedOutput token = runS3 token $ \b k -> 28 | void $ send $ deleteObject b k 29 | 30 | runS3 :: Token -> (BucketName -> ObjectKey -> AWS a) -> Handler a 31 | runS3 token f = do 32 | app <- getYesod 33 | 34 | let e = appAWSEnv app 35 | b = appS3Bucket $ appSettings app 36 | k = ObjectKey $ tokenText token 37 | 38 | runResourceT $ runAWS e $ f b k 39 | -------------------------------------------------------------------------------- /src/CommandContent.hs: -------------------------------------------------------------------------------- 1 | module CommandContent 2 | ( CommandContent(..) 3 | , findContent404 4 | , contentHeader 5 | , contentBody 6 | ) where 7 | 8 | import Import 9 | import Archive 10 | 11 | -- import Network.AWS 12 | -- import Network.AWS.S3 (_NoSuchKey) 13 | 14 | import qualified Data.ByteString.Lazy as BL 15 | 16 | data CommandContent 17 | = Live Token Command 18 | | Archived BL.ByteString 19 | 20 | findContent404 :: Token -> YesodDB App CommandContent 21 | findContent404 token = do 22 | mcommand <- getBy $ UniqueCommand token 23 | 24 | case mcommand of 25 | Just (Entity _ command) -> return $ Live token command 26 | _ -> lift $ Archived <$> archivedOutput token 27 | 28 | -- Need to deal with MonadCatch 29 | -- _ -> lift $ Archived 30 | -- <$> catching _NoSuchKey (archivedOutput token) (\_ -> notFound) 31 | 32 | contentHeader :: CommandContent -> Widget 33 | contentHeader (Live _ command) = [whamlet| 34 | 35 | #{show $ commandCreatedAt command} 36 | 37 | $maybe desc <- commandDescription command 38 | #{desc} 39 | $nothing 40 | No description 41 | |] 42 | contentHeader (Archived _) = [whamlet| 43 | Archived output 44 | |] 45 | 46 | 47 | contentBody :: CommandContent -> Widget 48 | contentBody (Live token _) = [whamlet| 49 | 50 | |] 51 | contentBody (Archived content) = [whamlet| 52 | #{decodeUtf8 content} 53 | |] 54 | -------------------------------------------------------------------------------- /src/Data/Time/Duration.hs: -------------------------------------------------------------------------------- 1 | -- | 2 | -- 3 | -- TODO: pick better names and release this as a small library 4 | -- 5 | module Data.Time.Duration 6 | ( since 7 | , priorTo 8 | , module Data.Time 9 | , module Data.Time.Units 10 | ) where 11 | 12 | import Prelude 13 | import Data.Time hiding (Day) 14 | import Data.Time.Units 15 | 16 | -- | Calculate a relative time since the given one 17 | -- 18 | -- > (5 :: Second) `since` now 19 | -- 20 | since :: TimeUnit a => a -> UTCTime -> UTCTime 21 | since s = addUTCTime $ toNominalDiffTime s 22 | 23 | -- | Calculate a relative time prior to the given one 24 | -- 25 | -- > (10 :: Day) `priorTo` now 26 | -- 27 | priorTo :: TimeUnit a => a -> UTCTime -> UTCTime 28 | priorTo s = addUTCTime (negate $ toNominalDiffTime s) 29 | 30 | toNominalDiffTime :: TimeUnit a => a -> NominalDiffTime 31 | toNominalDiffTime = fromInteger . (`div` 1000000) . toMicroseconds 32 | -------------------------------------------------------------------------------- /src/Foundation.hs: -------------------------------------------------------------------------------- 1 | module Foundation where 2 | 3 | import Import.NoFoundation 4 | 5 | import Database.Persist.Sql (ConnectionPool, runSqlPool) 6 | import Text.Hamlet (hamletFile) 7 | import Text.Jasmine (minifym) 8 | import Yesod.Core.Types (Logger) 9 | import Yesod.Default.Util (addStaticContentExternal) 10 | 11 | import qualified Network.AWS as AWS 12 | import qualified Yesod.Core.Unsafe as Unsafe 13 | 14 | data App = App 15 | { appSettings :: AppSettings 16 | , appStatic :: Static 17 | , appConnPool :: ConnectionPool 18 | , appHttpManager :: Manager 19 | , appLogger :: Logger 20 | , appAWSEnv :: AWS.Env 21 | } 22 | 23 | instance HasHttpManager App where 24 | getHttpManager = appHttpManager 25 | 26 | mkYesodData "App" $(parseRoutesFile "config/routes") 27 | 28 | type Form x = Html -> MForm (HandlerFor App) (FormResult x, Widget) 29 | 30 | instance Yesod App where 31 | approot = ApprootMaster $ appRoot . appSettings 32 | 33 | makeSessionBackend _ = Just <$> 34 | defaultClientSessionBackend 120 "config/client_session_key.aes" 35 | 36 | defaultLayout widget = do 37 | mmsg <- getMessage 38 | pc <- widgetToPageContent $ do 39 | addStylesheet $ StaticR css_screen_css 40 | $(widgetFile "default-layout") 41 | 42 | withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") 43 | 44 | addStaticContent ext mime content = do 45 | master <- getYesod 46 | let staticDir = appStaticDir $ appSettings master 47 | addStaticContentExternal 48 | minifym 49 | genFileName 50 | staticDir 51 | (StaticR . flip StaticRoute []) 52 | ext 53 | mime 54 | content 55 | where 56 | genFileName lbs = "autogen-" ++ base64md5 lbs 57 | 58 | shouldLogIO App{..} _source = pure. (appSettings `allowsLevel`) 59 | 60 | makeLogger = return . appLogger 61 | 62 | instance YesodPersist App where 63 | type YesodPersistBackend App = SqlBackend 64 | runDB action = do 65 | master <- getYesod 66 | runSqlPool action $ appConnPool master 67 | 68 | instance YesodPersistRunner App where 69 | getDBRunner = defaultGetDBRunner appConnPool 70 | 71 | instance RenderMessage App FormMessage where 72 | renderMessage _ _ = defaultFormMessage 73 | 74 | unsafeHandler :: App -> Handler a -> IO a 75 | unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger 76 | -------------------------------------------------------------------------------- /src/Handler/Command.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ScopedTypeVariables #-} 2 | 3 | module Handler.Command 4 | ( postCommandsR 5 | , patchCommandR 6 | , getCommandR 7 | , deleteCommandR 8 | , putCommandR 9 | ) 10 | where 11 | 12 | import Import 13 | import Archive 14 | import CommandContent 15 | 16 | -- import Network.AWS (catching) 17 | -- import Network.AWS.S3 (_NoSuchKey) 18 | 19 | data CommandRequest = CommandRequest 20 | { reqDescription :: Maybe Text } 21 | 22 | instance FromJSON CommandRequest where 23 | parseJSON = withObject "CommandRequest" $ \o -> CommandRequest 24 | <$> o .:? "description" 25 | 26 | postCommandsR :: Handler TypedContent 27 | postCommandsR = do 28 | now <- liftIO getCurrentTime 29 | req <- requireCheckJsonBody 30 | token <- newToken 31 | 32 | void $ runDB $ insert Command 33 | { commandToken = token 34 | , commandDescription = reqDescription req 35 | , commandCreatedAt = now 36 | } 37 | 38 | selectRep $ do 39 | provideRep (sendResponseStatus status201 $ tokenText token :: Handler Text) 40 | provideRep (sendResponseStatus status201 $ object ["token" .= token] :: Handler Value) 41 | provideRep (redirect $ CommandR token :: Handler Html) 42 | 43 | getCommandR :: Token -> Handler Html 44 | getCommandR token = do 45 | content <- runDB $ findContent404 token 46 | 47 | defaultLayout $ do 48 | setTitle "tee.io - Command" 49 | $(widgetFile "command") 50 | 51 | deleteCommandR :: Token -> Handler () 52 | deleteCommandR token = do 53 | runDB $ mapM_ (deleteCommand . entityKey) =<< getBy (UniqueCommand token) 54 | -- Need to deal with MonadCatch 55 | -- catching _NoSuchKey (deleteArchivedOutput token) $ \_ -> return () 56 | deleteArchivedOutput token `catch` \(_ :: SomeException) -> return () 57 | 58 | -- Deprecated. Originally we required callers to update commands to 59 | -- running:false so we could take steps to archive content to S3. We'll instead 60 | -- implement timeout semantics. 61 | patchCommandR :: Token -> Handler () 62 | patchCommandR _ = return () 63 | 64 | -- Deprecated. Originally wrote the API to accept PUT with PATCH semantics. We 65 | -- still except it for older clients. 66 | putCommandR :: Token -> Handler () 67 | putCommandR _ = return () 68 | -------------------------------------------------------------------------------- /src/Handler/Common.hs: -------------------------------------------------------------------------------- 1 | module Handler.Common where 2 | 3 | import Import 4 | 5 | import Data.FileEmbed (embedFile) 6 | 7 | getFaviconR :: Handler TypedContent 8 | getFaviconR = return 9 | $ TypedContent "image/x-icon" 10 | $ toContent $(embedFile "config/favicon.ico") 11 | 12 | getRobotsR :: Handler TypedContent 13 | getRobotsR = return 14 | $ TypedContent typePlain 15 | $ toContent $(embedFile "config/robots.txt") 16 | -------------------------------------------------------------------------------- /src/Handler/Home.hs: -------------------------------------------------------------------------------- 1 | module Handler.Home where 2 | 3 | import Import 4 | 5 | getHomeR :: Handler Html 6 | getHomeR = defaultLayout $ do 7 | setTitle "tee.io" 8 | $(widgetFile "homepage") 9 | -------------------------------------------------------------------------------- /src/Handler/Output.hs: -------------------------------------------------------------------------------- 1 | module Handler.Output 2 | ( postOutputR 3 | , getOutputR 4 | ) where 5 | 6 | import Import 7 | 8 | import Network.WebSockets (ConnectionException) 9 | import Yesod.WebSockets 10 | 11 | data OutputRequest = OutputRequest 12 | { reqContent :: Text 13 | } 14 | 15 | instance FromJSON OutputRequest where 16 | parseJSON = withObject "OutputRequest" $ \o -> OutputRequest 17 | <$> o .: "content" 18 | 19 | postOutputR :: Token -> Handler () 20 | postOutputR token = do 21 | now <- liftIO getCurrentTime 22 | req <- requireCheckJsonBody 23 | void $ runDB $ do 24 | Entity commandId _ <- getBy404 $ UniqueCommand token 25 | 26 | insert Output 27 | { outputCommand = commandId 28 | , outputContent = reqContent req 29 | , outputCreatedAt = now 30 | } 31 | 32 | sendResponseStatus status201 () 33 | 34 | getOutputR :: Token -> Handler () 35 | getOutputR token = do 36 | Entity commandId _ <- runDB $ getBy404 $ UniqueCommand token 37 | 38 | webSockets $ outputStream commandId 0 39 | 40 | outputStream :: CommandId -> Int -> WebSocketsT Handler () 41 | outputStream commandId start = catchingConnectionException $ do 42 | outputs <- lift $ runDB $ commandOutputs commandId start 43 | 44 | -- if we get no (more) output, check if the command is still live 45 | stop <- return (null outputs) &&^ not <$> commandExists 46 | 47 | unless stop $ do 48 | sendTextDataAck "" 49 | mapM_ (sendTextDataAck . outputContent) outputs 50 | 51 | outputStream commandId (start + length outputs) 52 | 53 | where 54 | commandExists = lift $ runDB $ exists [CommandId ==. commandId] 55 | 56 | catchingConnectionException :: WebSocketsT Handler () -> WebSocketsT Handler () 57 | catchingConnectionException f = f `catch` \e -> 58 | $(logWarn) $ pack $ show (e :: ConnectionException) 59 | 60 | sendTextDataAck :: MonadIO m => Text -> WebSocketsT m () 61 | sendTextDataAck msg = do 62 | sendTextData msg 63 | void receiveTextData 64 | 65 | -- Just fixing the type to Text 66 | receiveTextData :: MonadIO m => WebSocketsT m Text 67 | receiveTextData = receiveData 68 | 69 | -- In this context, it's important the second action isn't evaluated if the 70 | -- first gives @False@ (otherwise we'd make a needless DB query every output 71 | -- iteration). That rules out most existing implementations, including obvious 72 | -- ones like @liftM2 (&&)@. 73 | (&&^) :: Monad m => m Bool -> m Bool -> m Bool 74 | ma &&^ mb = ma >>= \a -> if a then mb else return False 75 | -------------------------------------------------------------------------------- /src/Import.hs: -------------------------------------------------------------------------------- 1 | module Import 2 | ( module Import 3 | ) where 4 | 5 | import Foundation as Import 6 | import Import.NoFoundation as Import 7 | -------------------------------------------------------------------------------- /src/Import/NoFoundation.hs: -------------------------------------------------------------------------------- 1 | module Import.NoFoundation 2 | ( module Import 3 | ) where 4 | 5 | import Model as Import 6 | import Settings as Import 7 | import Settings.StaticFiles as Import 8 | import Token as Import 9 | 10 | import ClassyPrelude.Yesod as Import 11 | import Data.Aeson as Import 12 | import Yesod.Core.Types as Import (loggerSet) 13 | import Yesod.Default.Config2 as Import 14 | -------------------------------------------------------------------------------- /src/Model.hs: -------------------------------------------------------------------------------- 1 | module Model where 2 | 3 | import Token 4 | import ClassyPrelude.Yesod 5 | import Database.Persist.Quasi 6 | 7 | share [mkPersist sqlSettings, mkMigrate "migrateAll"] 8 | $(persistFileWith lowerCaseSettings "config/models") 9 | 10 | exists 11 | :: ( MonadIO m 12 | , PersistQueryRead b 13 | , PersistRecordBackend v b 14 | , PersistEntity v 15 | ) 16 | => [Filter v] -> ReaderT b m Bool 17 | exists = fmap (> 0) . count 18 | 19 | commandOutputs :: MonadIO m => CommandId -> Int -> ReaderT SqlBackend m [Output] 20 | commandOutputs commandId start = map entityVal <$> selectList 21 | [OutputCommand ==. commandId] 22 | [Asc OutputCreatedAt, OffsetBy start] 23 | 24 | deleteCommand :: MonadIO m => CommandId -> ReaderT SqlBackend m () 25 | deleteCommand commandId = do 26 | deleteWhere [OutputCommand ==. commandId] 27 | delete commandId 28 | -------------------------------------------------------------------------------- /src/Network/PGDatabaseURL.hs: -------------------------------------------------------------------------------- 1 | module Network.PGDatabaseURL 2 | ( parsePGConnectionString 3 | ) where 4 | 5 | import Prelude 6 | 7 | import Data.Maybe (isJust) 8 | import Data.String (IsString(..)) 9 | import Network.HTTP.Base 10 | ( host 11 | , parseURIAuthority 12 | , password 13 | , port 14 | , uriToAuthorityString 15 | , user 16 | ) 17 | import Network.URI (parseURI, uriPath, uriScheme) 18 | 19 | data PGConnection = PGConnection 20 | { connUser :: Maybe String 21 | , connPassword :: Maybe String 22 | , connHost :: Maybe String 23 | , connPort :: Maybe Int 24 | , connDBname :: Maybe String 25 | } 26 | 27 | parsePGConnectionString :: IsString a => String -> Either String a 28 | parsePGConnectionString = fmap toConnectionString . parsePGDatabaseURL 29 | 30 | parsePGDatabaseURL :: String -> Either String PGConnection 31 | parsePGDatabaseURL url = do 32 | uri <- checkP "URI" parseURI url 33 | uriAuth <- checkP "Authority" parseURIAuthority $ uriToAuthorityString uri 34 | checkScheme $ uriScheme uri 35 | 36 | return PGConnection 37 | { connUser = user uriAuth 38 | , connPassword = password uriAuth 39 | , connHost = emptyToMaybe $ host uriAuth 40 | , connPort = port uriAuth 41 | , connDBname = emptyToMaybe $ dropChar '/' $ uriPath uri 42 | } 43 | 44 | toConnectionString :: IsString a => PGConnection -> a 45 | toConnectionString c = fromString $ unwords 46 | -- N.B. Pattern match is safe because of preceding filter 47 | $ map (\(k, Just v) -> k ++ "=" ++ v) 48 | $ filter (isJust . snd) 49 | [ ("user", connUser c) 50 | , ("password", connPassword c) 51 | , ("host", connHost c) 52 | , ("port", show <$> connPort c) 53 | , ("dbname", connDBname c) 54 | ] 55 | 56 | checkScheme :: String -> Either String () 57 | checkScheme "postgres:" = return () 58 | checkScheme x = Left $ 59 | "Invalid scheme: " ++ x ++ "//, expecting postgres://" 60 | 61 | checkP :: String -> (String -> Maybe a) -> String -> Either String a 62 | checkP label p x = check ("Invalid " ++ label ++ ": " ++ x) $ p x 63 | 64 | check :: String -> Maybe a -> Either String a 65 | check msg = maybe (Left msg) Right 66 | 67 | emptyToMaybe :: [a] -> Maybe [a] 68 | emptyToMaybe [] = Nothing 69 | emptyToMaybe x = Just x 70 | 71 | dropChar :: Char -> String -> String 72 | dropChar c (x:xs) | c == x = xs 73 | dropChar _ x = x 74 | -------------------------------------------------------------------------------- /src/Network/S3URL.hs: -------------------------------------------------------------------------------- 1 | module Network.S3URL 2 | ( S3URL(..) 3 | , parseS3URL 4 | ) where 5 | 6 | import Prelude 7 | 8 | import Data.Aeson 9 | import Data.Text (Text) 10 | import Data.Maybe (fromMaybe) 11 | import Data.Monoid ((<>)) 12 | import Control.Lens (view) 13 | import Network.URI 14 | import Text.Read (readMaybe) 15 | import Network.AWS (Region(..), Service, endpointHost) 16 | import Network.AWS.Endpoint (defaultEndpoint, setEndpoint) 17 | import Network.AWS.S3 (BucketName(..), s3) 18 | 19 | import qualified Data.ByteString.Char8 as C8 20 | import qualified Data.Text as T 21 | 22 | data S3URL = S3URL 23 | { s3Service :: Service 24 | , s3Bucket :: BucketName 25 | } 26 | 27 | instance FromJSON S3URL where 28 | parseJSON = withText "URL" $ 29 | either (fail . ("Invalid S3 URL: " <>)) return . parseS3URL 30 | 31 | parseS3URL :: Text -> Either String S3URL 32 | parseS3URL t = do 33 | uri <- wrap "invalid URI" $ parseURI $ T.unpack t 34 | 35 | let auth = fromMaybe (URIAuth "" "" "") $ uriAuthority uri 36 | host = C8.pack $ if null $ uriRegName auth 37 | then defaultS3Host 38 | else uriRegName auth 39 | 40 | port <- case uriPort auth of 41 | (':':x) -> wrap "invalid port" $ readMaybe x 42 | _ -> wrap "cannot infer port" $ portForScheme $ uriScheme uri 43 | 44 | bucket <- wrap "bucket not provided" $ case uriPath uri of 45 | ('/':x:xs) -> Just $ x:xs 46 | _ -> Nothing 47 | 48 | return S3URL 49 | { s3Service = setEndpoint (uriScheme uri == "https:") host port s3 50 | , s3Bucket = BucketName $ T.pack bucket 51 | } 52 | 53 | where 54 | wrap msg = maybe (Left msg) Right 55 | 56 | defaultS3Host = C8.unpack 57 | $ view endpointHost 58 | $ defaultEndpoint s3 NorthVirginia 59 | 60 | portForScheme "http:" = Just 80 61 | portForScheme "https:" = Just 443 62 | portForScheme _ = Nothing 63 | -------------------------------------------------------------------------------- /src/Settings.hs: -------------------------------------------------------------------------------- 1 | module Settings where 2 | 3 | import ClassyPrelude.Yesod 4 | import Control.Exception (throw) 5 | import Data.Aeson (Result(..), fromJSON, withObject) 6 | import Data.Aeson.Types (Parser) 7 | import Data.FileEmbed (embedFile) 8 | import Data.Time.Units (Second) 9 | import Data.Yaml (decodeEither') 10 | import Database.Persist.Postgresql (PostgresConf(..)) 11 | import Language.Haskell.TH.Syntax (Exp, Q) 12 | import Network.AWS (Service) 13 | import Network.AWS.S3 (BucketName(..)) 14 | import Network.S3URL (S3URL(..)) 15 | import Network.PGDatabaseURL (parsePGConnectionString) 16 | import Network.Wai.Handler.Warp (HostPreference) 17 | import Yesod.Default.Config2 (applyEnvValue, configSettingsYml) 18 | import Yesod.Default.Util 19 | #if DEVELOPMENT 20 | (widgetFileReload) 21 | #else 22 | (widgetFileNoReload) 23 | #endif 24 | 25 | import qualified Data.Text as T 26 | import qualified Data.ByteString.Char8 as C8 27 | 28 | data AppSettings = AppSettings 29 | { appStaticDir :: String 30 | , appDatabaseConf :: PostgresConf 31 | , appRoot :: Text 32 | , appHost :: HostPreference 33 | , appPort :: Int 34 | , appIpFromHeader :: Bool 35 | , appCommandTimeout :: Second 36 | , appS3Service :: Service 37 | , appS3Bucket :: BucketName 38 | , appLogLevel :: LogLevel 39 | , appMutableStatic :: Bool 40 | } 41 | 42 | instance Show AppSettings where 43 | show AppSettings{..} = concat 44 | [ "log_level=", show appLogLevel 45 | , " host=", show appHost 46 | , " port=", show appPort 47 | , " root=", show appRoot 48 | , " db=[", C8.unpack $ pgConnStr appDatabaseConf, "]" 49 | , " s3_bucket=", (\(BucketName t) -> T.unpack t) appS3Bucket 50 | , " command_timeout=", show appCommandTimeout 51 | ] 52 | 53 | instance FromJSON AppSettings where 54 | parseJSON = withObject "AppSettings" $ \o -> do 55 | url <- o .: "database-url" 56 | connStr <- either fail return $ parsePGConnectionString url 57 | 58 | appDatabaseConf <- PostgresConf 59 | <$> pure connStr 60 | <*> o .: "database-pool-size" 61 | appRoot <- o .: "approot" 62 | appHost <- fromString <$> o .: "host" 63 | appPort <- o .: "port" 64 | appIpFromHeader <- o .: "ip-from-header" 65 | appCommandTimeout <- fromIntegral 66 | <$> (o .: "command-timeout" :: Parser Integer) 67 | S3URL appS3Service appS3Bucket <- o .: "s3-url" 68 | appLogLevel <- parseLogLevel <$> o .: "log-level" 69 | appMutableStatic <- o .: "mutable-static" 70 | 71 | -- This value is needed in a pure context, and so can't read from ENV. 72 | -- It also doesn't differ between environments, so we might as well 73 | -- harcode it. 74 | let appStaticDir = "static" 75 | 76 | return AppSettings{..} 77 | 78 | where 79 | parseLogLevel :: Text -> LogLevel 80 | parseLogLevel t = case T.toLower t of 81 | "debug" -> LevelDebug 82 | "info" -> LevelInfo 83 | "warn" -> LevelWarn 84 | "error" -> LevelError 85 | _ -> LevelOther t 86 | 87 | allowsLevel :: AppSettings -> LogLevel -> Bool 88 | allowsLevel AppSettings{..} = (>= appLogLevel) 89 | 90 | widgetFile :: String -> Q Exp 91 | widgetFile = 92 | #if DEVELOPMENT 93 | widgetFileReload 94 | #else 95 | widgetFileNoReload 96 | #endif 97 | def 98 | 99 | configSettingsYmlBS :: ByteString 100 | configSettingsYmlBS = $(embedFile configSettingsYml) 101 | 102 | configSettingsYmlValue :: Value 103 | configSettingsYmlValue = either throw id $ decodeEither' configSettingsYmlBS 104 | 105 | compileTimeAppSettings :: AppSettings 106 | compileTimeAppSettings = 107 | case fromJSON $ applyEnvValue False mempty configSettingsYmlValue of 108 | Error e -> error e 109 | Success settings -> settings 110 | -------------------------------------------------------------------------------- /src/Settings/StaticFiles.hs: -------------------------------------------------------------------------------- 1 | module Settings.StaticFiles where 2 | 3 | import Settings (appStaticDir, compileTimeAppSettings) 4 | 5 | import Yesod.Static (staticFiles) 6 | 7 | staticFiles (appStaticDir compileTimeAppSettings) 8 | -------------------------------------------------------------------------------- /src/Token.hs: -------------------------------------------------------------------------------- 1 | module Token 2 | ( Token(..) 3 | , newToken 4 | , tokenText 5 | ) where 6 | 7 | import ClassyPrelude.Yesod 8 | import Data.UUID 9 | import Database.Persist.Sql 10 | import System.Random 11 | 12 | newtype Token = Token { tokenUUID :: UUID } 13 | deriving (Eq, Random, Read, Show) 14 | 15 | newToken :: MonadIO m => m Token 16 | newToken = liftIO randomIO 17 | 18 | tokenText :: Token -> Text 19 | tokenText = toText . tokenUUID 20 | 21 | fromTextEither :: Text -> Either Text Token 22 | fromTextEither x = case fromText x of 23 | Just y -> Right $ Token y 24 | Nothing -> Left $ "Invalid UUID: " <> x 25 | 26 | instance ToJSON Token where 27 | toJSON = toJSON . tokenText 28 | 29 | instance PathPiece Token where 30 | toPathPiece = tokenText 31 | fromPathPiece = fmap Token . fromText 32 | 33 | instance PersistField Token where 34 | toPersistValue = toPersistValue . tokenText 35 | fromPersistValue = fromTextEither <=< fromPersistValue 36 | 37 | instance PersistFieldSql Token where 38 | sqlType _ = SqlString 39 | -------------------------------------------------------------------------------- /src/Worker.hs: -------------------------------------------------------------------------------- 1 | module Worker 2 | ( workerMain 3 | 4 | -- Exported for testing 5 | , archivableCommands 6 | ) where 7 | 8 | import Import hiding 9 | ( (<.) 10 | , (=.) 11 | , (==.) 12 | , (>.) 13 | , (||.) 14 | , delete 15 | , isNothing 16 | , on 17 | , timeout 18 | , update 19 | ) 20 | 21 | import Archive 22 | import Application (handler) 23 | 24 | import Data.Time.Duration 25 | import Database.Esqueleto 26 | 27 | import qualified Data.Text as T 28 | 29 | workerMain :: IO () 30 | workerMain = handler $ do 31 | timeout <- appCommandTimeout . appSettings <$> getYesod 32 | archiveCommands timeout 33 | 34 | archiveCommands :: Second -> Handler () 35 | archiveCommands timeout = runDB $ do 36 | commands <- archivableCommands timeout 37 | 38 | $(logInfo) $ "archive_commands count=" <> T.pack (show $ length commands) 39 | 40 | mapM_ archiveCommand commands 41 | 42 | archivableCommands :: MonadIO m => Second -> ReaderT SqlBackend m [Entity Command] 43 | archivableCommands timeout = do 44 | cutoff <- (timeout `priorTo`) <$> liftIO getCurrentTime 45 | select $ from $ \(c `LeftOuterJoin` mo) -> do 46 | on ((just (c ^. CommandId) ==. mo ?. OutputCommand) &&. 47 | (mo ?. OutputCreatedAt >. just (val cutoff))) 48 | where_ ((c ^. CommandCreatedAt <. val cutoff) &&. 49 | isNothing (mo ?. OutputId)) 50 | return c 51 | 52 | archiveCommand :: Entity Command -> ReaderT SqlBackend Handler () 53 | archiveCommand (Entity commandId command) = do 54 | outputs <- commandOutputs commandId 0 55 | lift $ archiveOutput (commandToken command) outputs 56 | 57 | deleteCommand commandId 58 | 59 | $(logInfo) $ "archived token=" <> tokenText (commandToken command) 60 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-13.17 2 | extra-deps: 3 | - hspec-expectations-lifted-0.8.2 4 | - load-env-0.1.1 5 | - time-units-1.0.0 6 | -------------------------------------------------------------------------------- /stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: 7 | - completed: 8 | hackage: hspec-expectations-lifted-0.8.2@sha256:87e9c36db96e8719446363fab4946951897603b3b36274d5206d0cfe73a14bb9,788 9 | pantry-tree: 10 | size: 247 11 | sha256: 63e9790768f7b45d0a689a67a6d69bdc58ddd010f822b8a2e05b1cbee719727b 12 | original: 13 | hackage: hspec-expectations-lifted-0.8.2 14 | - completed: 15 | hackage: load-env-0.1.1@sha256:4332c4302837ff93f1fe2b9a656762e835744e285ba47f948e6d68aa33947b0c,1521 16 | pantry-tree: 17 | size: 319 18 | sha256: 6b7392d2e18b19b0ea36c433622db811bb477c8952652a9c4857bd98be552297 19 | original: 20 | hackage: load-env-0.1.1 21 | - completed: 22 | hackage: time-units-1.0.0@sha256:27cf54091c4a0ca73d504fc11d5c31ab4041d17404fe3499945e2055697746c1,928 23 | pantry-tree: 24 | size: 212 25 | sha256: 9b516d4195fcea22cd1f4335cfe210e88deca397ba7dacc494d5a2feb69e1af8 26 | original: 27 | hackage: time-units-1.0.0 28 | snapshots: 29 | - completed: 30 | size: 497508 31 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/13/17.yaml 32 | sha256: 3d8fabe77d4f7618554cfb1001c820b9859820b8639bfd6f02a1c41660afb53b 33 | original: lts-13.17 34 | -------------------------------------------------------------------------------- /static/css/screen.css: -------------------------------------------------------------------------------- 1 | img { 2 | margin: 5px; 3 | margin-bottom: 25px; 4 | max-width: 95%; 5 | 6 | -webkit-box-shadow: 2px 12px 20px #888; 7 | -moz-box-shadow: 2px 12px 20px #888; 8 | box-shadow: 2px 12px 20px #888; 9 | } 10 | 11 | header { 12 | border-bottom: 1px solid; 13 | margin-top: 1em; 14 | padding-bottom: 0.5em; 15 | } 16 | 17 | footer { 18 | border-top: 1px solid; 19 | margin-bottom: 1em; 20 | padding-top: 0.5em; 21 | } 22 | 23 | .container { 24 | margin: auto; 25 | max-width: 760px; 26 | } 27 | 28 | .right { 29 | float: right; 30 | } 31 | 32 | .indent { 33 | margin-left: 4em; 34 | } 35 | -------------------------------------------------------------------------------- /static/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbrisbin/tee-io/7f914cf7c40a4a9331c61feb8cd432b61ae5eb4e/static/demo.gif -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbrisbin/tee-io/7f914cf7c40a4a9331c61feb8cd432b61ae5eb4e/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbrisbin/tee-io/7f914cf7c40a4a9331c61feb8cd432b61ae5eb4e/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbrisbin/tee-io/7f914cf7c40a4a9331c61feb8cd432b61ae5eb4e/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/tee-io: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | usage() { 5 | cat <&2; exit 64 ;; 42 | esac 43 | 44 | shift 45 | done 46 | 47 | if [ -n "$description" ]; then 48 | token=$(curl_json "/commands" <= 1.8 4 | build-type: Simple 5 | 6 | flag dev 7 | description: Turn auto-reload templates. 8 | default: False 9 | 10 | flag library-only 11 | description: Build for use with "yesod devel" 12 | default: False 13 | 14 | library 15 | hs-source-dirs: src, app 16 | exposed-modules: Data.Time.Duration 17 | Network.PGDatabaseURL 18 | Network.S3URL 19 | Application 20 | Foundation 21 | Import 22 | Import.NoFoundation 23 | Token 24 | Model 25 | Settings 26 | Settings.StaticFiles 27 | Handler.Common 28 | Handler.Home 29 | Handler.Command 30 | Handler.Output 31 | Archive 32 | CommandContent 33 | Worker 34 | 35 | if flag(dev) || flag(library-only) 36 | cpp-options: -DDEVELOPMENT 37 | ghc-options: -Wall -Werror -fwarn-tabs -O0 38 | else 39 | ghc-options: -Wall -Werror -fwarn-tabs -O2 40 | 41 | extensions: CPP 42 | DeriveDataTypeable 43 | EmptyDataDecls 44 | FlexibleContexts 45 | FlexibleInstances 46 | GADTs 47 | GeneralizedNewtypeDeriving 48 | MultiParamTypeClasses 49 | NoImplicitPrelude 50 | NoMonomorphismRestriction 51 | OverloadedStrings 52 | QuasiQuotes 53 | RecordWildCards 54 | TemplateHaskell 55 | TupleSections 56 | TypeFamilies 57 | ViewPatterns 58 | 59 | build-depends: base 60 | , aeson 61 | , amazonka 62 | , amazonka-core 63 | , amazonka-s3 64 | , bytestring 65 | , classy-prelude 66 | , classy-prelude-conduit 67 | , classy-prelude-yesod 68 | , conduit 69 | , conduit-extra 70 | , containers 71 | , data-default 72 | , directory 73 | , esqueleto 74 | , fast-logger 75 | , file-embed 76 | , foreign-store 77 | , hjsmin 78 | , http-conduit 79 | , http-types 80 | , lens 81 | , load-env 82 | , monad-control 83 | , monad-logger 84 | , network-uri 85 | , persistent 86 | , persistent-postgresql 87 | , random 88 | , safe 89 | , scientific 90 | , shakespeare 91 | , template-haskell 92 | , text 93 | , time 94 | , time-units 95 | , transformers 96 | , unordered-containers 97 | , uuid 98 | , vector 99 | , wai 100 | , wai-extra 101 | , wai-logger 102 | , warp 103 | , websockets 104 | , yaml 105 | , yesod 106 | , yesod-core 107 | , yesod-static 108 | , yesod-websockets 109 | , HTTP 110 | 111 | executable tee-io 112 | if flag(library-only) 113 | buildable: False 114 | 115 | main-is: main.hs 116 | hs-source-dirs: app 117 | build-depends: base 118 | , tee-io 119 | 120 | ghc-options: -Wall -Werror -threaded -O2 -rtsopts -with-rtsopts=-N 121 | 122 | executable tee-io-worker 123 | if flag(library-only) 124 | buildable: False 125 | 126 | main-is: main-worker.hs 127 | hs-source-dirs: app 128 | build-depends: base 129 | , tee-io 130 | 131 | ghc-options: -Wall -Werror -threaded -O2 -rtsopts -with-rtsopts=-N 132 | 133 | test-suite test 134 | type: exitcode-stdio-1.0 135 | main-is: Spec.hs 136 | hs-source-dirs: test 137 | ghc-options: -Wall -Werror 138 | 139 | extensions: CPP 140 | DeriveDataTypeable 141 | EmptyDataDecls 142 | FlexibleContexts 143 | FlexibleInstances 144 | GADTs 145 | GeneralizedNewtypeDeriving 146 | MultiParamTypeClasses 147 | NoImplicitPrelude 148 | NoMonomorphismRestriction 149 | OverloadedStrings 150 | QuasiQuotes 151 | RecordWildCards 152 | TemplateHaskell 153 | TupleSections 154 | TypeFamilies 155 | ViewPatterns 156 | 157 | build-depends: base 158 | , tee-io 159 | , hspec 160 | , hspec-expectations-lifted 161 | , aeson 162 | , amazonka 163 | , amazonka-s3 164 | , bytestring 165 | , classy-prelude 166 | , http-types 167 | , lens 168 | , load-env 169 | , persistent 170 | , shakespeare 171 | , uuid 172 | , wai-extra 173 | , yesod 174 | , yesod-core 175 | , yesod-persistent 176 | , yesod-test 177 | -------------------------------------------------------------------------------- /templates/command.hamlet: -------------------------------------------------------------------------------- 1 |
2 | ^{contentHeader content} 3 | 4 |
5 |   ^{contentBody content}
6 | 


--------------------------------------------------------------------------------
/templates/command.julius:
--------------------------------------------------------------------------------
 1 | function Output(elem) {
 2 |   this.elem = elem;
 3 |   this.streamUrl = elem.
 4 |     getAttribute("data-stream-url").
 5 |     replace(/http(s?):/, "ws$1:");
 6 | }
 7 | 
 8 | Output.prototype.append = function(text) {
 9 |   this.elem.textContent += text
10 | 
11 |   if (text != "") {
12 |     window.scrollTo(0, document.body.scrollHeight);
13 |   }
14 | };
15 | 
16 | Output.prototype.connect = function() {
17 |   var self = this
18 |     , socket = new WebSocket(this.streamUrl);
19 | 
20 |   socket.onerror = function(data) {
21 |     console.log("websockets error", data);
22 |   };
23 | 
24 |   socket.onmessage = function(data) {
25 |     self.append(data.data);
26 |     socket.send("acknowledged");
27 |   };
28 | }
29 | 
30 | document.addEventListener("DOMContentLoaded", function() {
31 |   var elem = document.getElementById("output")
32 | 
33 |   if (elem) {
34 |     new Output(elem).connect();
35 |   }
36 | });
37 | 


--------------------------------------------------------------------------------
/templates/default-layout-wrapper.hamlet:
--------------------------------------------------------------------------------
 1 | $newline never
 2 | \
 3 | 
 4 |   
 5 |     
 6 | 
 7 |     #{pageTitle pc}
 8 |     <meta name="description" content="">
 9 |     <meta name="author" content="">
10 |     <meta name="viewport" content="width=device-width,initial-scale=1">
11 | 
12 |     ^{pageHead pc}
13 |   <body>
14 |     <div .container>
15 |       ^{pageBody pc}
16 | 
17 |       <footer>
18 |         <small>
19 |           <span .right>
20 |             <a href="https://github.com/pbrisbin/tee-io">source
21 |           <a href=@{HomeR}>tee.io
22 |           \ - built by pat brisbin
23 | 


--------------------------------------------------------------------------------
/templates/default-layout.hamlet:
--------------------------------------------------------------------------------
1 | $maybe msg <- mmsg
2 |   <div #message>#{msg}
3 | ^{widget}
4 | 


--------------------------------------------------------------------------------
/templates/homepage.hamlet:
--------------------------------------------------------------------------------
 1 | <h1>
 2 |   <code>
 3 |     tee.io
 4 | 
 5 | <p>
 6 |   It can often be useful to execute a command, then both monitor and capture its
 7 |   output as it's being produced. This is what
 8 |   <a href="http://linux.die.net/man/1/tee">
 9 |     <code>
10 |       tee(1)
11 |   is good for.
12 |   <code>
13 |     tee.io
14 |   is sort of like that for commands on
15 |   <em>remote
16 |   systems.
17 | 
18 | <p>
19 |   It's like
20 |   <code>tee(1)
21 |   as a service.
22 | 
23 | <img alt="demo" src=@{StaticR demo_gif}>
24 | 
25 | <h2>Usage
26 | 
27 | <ul>
28 |   <li>
29 |     <a href="http://docs.teeio.apiary.io">API Documentation
30 |   <li>
31 |     <a href="https://rubygems.org/gems/tee-io">Ruby SDK
32 |   <li>
33 |     <a href=@{StaticR tee_io}>command-line client
34 | 
35 | <h2>Motivation
36 | 
37 | <p>
38 |   At
39 |   <a href="https://codeclimate.com">Code Climate</a>,
40 |   we frequently run commands on remote systems via a bot in our team chat
41 |   room.
42 | 
43 | <p>
44 |   We would normally capture the
45 |   <code>stdout
46 |   and
47 |   <code>stderr
48 |   from these processes, manipulate it, format it, and post it back to chat. This
49 |   has a number of downsides:
50 | 
51 | <ul>
52 |   <li>The logic is non-trivial and situationally-dependent
53 |   <li>The output seriously clutters the chat history
54 |   <li>We have to wait for the command to complete before we see anything
55 | 
56 | <p>
57 |   To solve these problems, I built
58 |   <code>tee.io</code>.
59 |   Maybe it'll be useful to you too.
60 | 


--------------------------------------------------------------------------------
/test/Data/Time/DurationSpec.hs:
--------------------------------------------------------------------------------
 1 | module Data.Time.DurationSpec
 2 |     ( main
 3 |     , spec
 4 |     ) where
 5 | 
 6 | import Prelude
 7 | import Test.Hspec
 8 | import Data.Time.Duration
 9 | 
10 | main :: IO ()
11 | main = hspec spec
12 | 
13 | spec :: Spec
14 | spec = describe "Data.Time.Duration" $ do
15 |     describe "since" $ it "works" $ do
16 |         let t1 = mkTime "2015-12-31 05:15:00"
17 |             t2 = (6 :: Second) `since` t1
18 | 
19 |         t2 `shouldBe` mkTime "2015-12-31 05:15:06"
20 | 
21 |     describe "priorTo" $ it "works" $ do
22 |         let t1 = mkTime "2015-12-31 05:15:00"
23 |             t2 = (2 :: Day) `priorTo` t1
24 | 
25 |         t2 `shouldBe` mkTime "2015-12-29 05:15:00"
26 | 
27 | mkTime :: String -> UTCTime
28 | mkTime = parseTimeOrError False defaultTimeLocale "%F %T"
29 | 


--------------------------------------------------------------------------------
/test/Handler/CommandSpec.hs:
--------------------------------------------------------------------------------
 1 | module Handler.CommandSpec
 2 |     ( main
 3 |     , spec
 4 |     ) where
 5 | 
 6 | import SpecHelper
 7 | import Data.UUID (fromText)
 8 | 
 9 | data Response = Response Token
10 | 
11 | instance FromJSON Response where
12 |     parseJSON = withObject "Response" $ \o -> Response
13 |         <$> (parseToken =<< o .: "token")
14 | 
15 |       where
16 |         parseToken = maybe mzero (return . Token) . fromText
17 | 
18 | main :: IO ()
19 | main = hspec spec
20 | 
21 | spec :: Spec
22 | spec = withApp $ do
23 |     describe "POST /commands" $ do
24 |         it "creates a new command" $ do
25 |             postJSON CommandsR $ object []
26 | 
27 |             withJSONResponse $ \(Response token) -> do
28 |                 Entity _ command <- runDB $ getBy404 $ UniqueCommand token
29 |                 commandDescription command `shouldBe` Nothing
30 | 
31 |         it "creates a command with a description" $ do
32 |             postJSON CommandsR $ object ["description" .= ("test command" :: Text)]
33 | 
34 |             withJSONResponse $ \(Response token) -> do
35 |                 Entity _ command <- runDB $ getBy404 $ UniqueCommand token
36 |                 commandDescription command `shouldBe` Just "test command"
37 | 
38 |     describe "DELETE /commands/token" $
39 |         it "deletes the command's data" $ do
40 |             now <- liftIO getCurrentTime
41 |             token <- newToken
42 |             void $ runDB $ insert Command
43 |                 { commandToken = token
44 |                 , commandDescription = Just "a description"
45 |                 , commandCreatedAt = now
46 |                 }
47 | 
48 |             delete $ CommandR token
49 |             statusIs 200
50 | 
51 |             results <- runDB $ selectList [CommandToken ==. token] []
52 |             results `shouldBe` []
53 | 


--------------------------------------------------------------------------------
/test/Handler/HomeSpec.hs:
--------------------------------------------------------------------------------
 1 | module Handler.HomeSpec
 2 |     ( main
 3 |     , spec
 4 |     ) where
 5 | 
 6 | import SpecHelper
 7 | 
 8 | main :: IO ()
 9 | main = hspec spec
10 | 
11 | spec :: Spec
12 | spec = withApp $
13 |     it "loads successfully" $ do
14 |         get HomeR
15 |         statusIs 200
16 | 


--------------------------------------------------------------------------------
/test/Handler/OutputSpec.hs:
--------------------------------------------------------------------------------
 1 | module Handler.OutputSpec
 2 |     ( main
 3 |     , spec
 4 |     ) where
 5 | 
 6 | import SpecHelper
 7 | 
 8 | main :: IO ()
 9 | main = hspec spec
10 | 
11 | spec :: Spec
12 | spec = withApp $ do
13 |     describe "POST /commands/token/output" $
14 |         it "creates command output" $ do
15 |             now <- liftIO getCurrentTime
16 |             token <- newToken
17 |             void $ runDB $ insert Command
18 |                     { commandToken = token
19 |                     , commandDescription = Nothing
20 |                     , commandCreatedAt = now
21 |                     }
22 | 
23 |             postJSON (OutputR token) $ object ["content" .= ("line 1\n" :: Text)]
24 |             postJSON (OutputR token) $ object ["content" .= ("line 2\n" :: Text)]
25 |             postJSON (OutputR token) $ object ["content" .= ("line 3\n" :: Text)]
26 | 
27 |             outputs <- runDB $ selectList [] []
28 |             map (outputContent . entityVal) outputs `shouldBe`
29 |                 [ "line 1\n"
30 |                 , "line 2\n"
31 |                 , "line 3\n"
32 |                 ]
33 | 
34 |     describe "GET /commands/token/output" $
35 |         it "streams output via websockets" $ do
36 |             now <- liftIO getCurrentTime
37 |             token <- newToken
38 |             void $ runDB $ do
39 |                 commandId <- insert Command
40 |                     { commandToken = token
41 |                     , commandDescription = Nothing
42 |                     , commandCreatedAt = now
43 |                     }
44 | 
45 |                 void $ insert Output
46 |                     { outputCommand = commandId
47 |                     , outputContent = "line 1\n"
48 |                     , outputCreatedAt = now
49 |                     }
50 |                 void $ insert Output
51 |                     { outputCommand = commandId
52 |                     , outputContent = "line 2\n"
53 |                     , outputCreatedAt = now
54 |                     }
55 | 
56 |             -- TODO somehow run the websocket producer with a test client and
57 |             -- assert we get the two lines of output
58 |             get $ OutputR token
59 | 
60 |             statusIs 200
61 | 


--------------------------------------------------------------------------------
/test/Network/PGDatabaseURLSpec.hs:
--------------------------------------------------------------------------------
 1 | module Network.PGDatabaseURLSpec
 2 |     ( main
 3 |     , spec
 4 |     ) where
 5 | 
 6 | import Prelude
 7 | 
 8 | import Network.PGDatabaseURL
 9 | 
10 | import Test.Hspec
11 | 
12 | main :: IO ()
13 | main = hspec spec
14 | 
15 | spec :: Spec
16 | spec = describe "PGDatabaseURLSpec" $ do
17 |     it "parses a full URI" $
18 |         "postgres://a:b@c:123/d" `shouldParseTo` "user=a password=b host=c port=123 dbname=d"
19 | 
20 |     it "parses a URI without password" $
21 |         "postgres://a@c:123/d" `shouldParseTo` "user=a host=c port=123 dbname=d"
22 | 
23 |     it "parses a URI without credentials" $
24 |         "postgres://c:123/d" `shouldParseTo` "host=c port=123 dbname=d"
25 | 
26 |     it "parses a URI without port" $
27 |         "postgres://c/d" `shouldParseTo` "host=c dbname=d"
28 | 
29 |     it "parses a URI without anything" $
30 |         "postgres:///" `shouldParseTo` ""
31 | 
32 |     it "rejects invalid URIs" $
33 |         "|7^bvk3" `shouldRejectWith` "Invalid URI: |7^bvk3"
34 | 
35 |     -- N.B. Can't figure out a way to make *just* the Authority invalid
36 |     -- it "rejects invalid Authorities" $ do
37 | 
38 |     it "rejects unexpected schemes" $
39 |         "https://example.com" `shouldRejectWith` "Invalid scheme: https://, expecting postgres://"
40 | 
41 | shouldParseTo :: String -> String -> Expectation
42 | x `shouldParseTo` y = parsePGConnectionString x `shouldBe` Right y
43 | 
44 | shouldRejectWith :: String -> String -> Expectation
45 | x `shouldRejectWith` y = do
46 |     let x' = parsePGConnectionString x :: Either String String
47 | 
48 |     x' `shouldBe` Left y
49 | 


--------------------------------------------------------------------------------
/test/Network/S3URLSpec.hs:
--------------------------------------------------------------------------------
 1 | module Network.S3URLSpec
 2 |     ( main
 3 |     , spec
 4 |     ) where
 5 | 
 6 | import Prelude
 7 | 
 8 | import Control.Lens
 9 | import Data.Aeson
10 | import Data.Monoid ((<>))
11 | import Network.S3URL
12 | import Network.AWS
13 |     ( Endpoint
14 |     , Region(..)
15 |     , _svcEndpoint
16 |     , endpointHost
17 |     , endpointPort
18 |     , endpointSecure
19 |     )
20 | import Network.AWS.S3 (BucketName(..))
21 | import Test.Hspec
22 | 
23 | import qualified Data.ByteString.Lazy.Char8 as C8
24 | 
25 | main :: IO ()
26 | main = hspec spec
27 | 
28 | spec :: Spec
29 | spec = describe "S3URL" $ do
30 |     it "parses from JSON" $
31 |         withDecoded "http://localhost:4569/my-bucket" $ \url -> do
32 |             let ep = serviceEndpoint url
33 | 
34 |             view endpointSecure ep `shouldBe` False
35 |             view endpointHost ep `shouldBe` "localhost"
36 |             view endpointPort ep `shouldBe` 4569
37 | 
38 |             s3Bucket url `shouldBe` BucketName "my-bucket"
39 | 
40 |     it "parses from JSON without authority" $
41 |         withDecoded "http:///my-bucket" $ \url -> do
42 |             let ep = serviceEndpoint url
43 | 
44 |             view endpointSecure ep `shouldBe` False
45 |             view endpointHost ep `shouldBe` "s3.amazonaws.com"
46 |             view endpointPort ep `shouldBe` 80
47 | 
48 |             s3Bucket url `shouldBe` BucketName "my-bucket"
49 | 
50 |     it "parses from JSON without port" $
51 |         withDecoded "https://localhost/my-bucket" $ \url -> do
52 |             let ep = serviceEndpoint url
53 | 
54 |             view endpointSecure ep `shouldBe` True
55 |             view endpointHost ep `shouldBe` "localhost"
56 |             view endpointPort ep `shouldBe` 443
57 | 
58 |     it "has nice parse errors" $ do
59 |         let Left err1 = eitherDecode "\"ftp://invalid\"" :: Either String S3URL
60 |         let Left err2 = eitherDecode "\"https://localhost\"" :: Either String S3URL
61 | 
62 |         err1 `shouldEndWith` "Invalid S3 URL: cannot infer port"
63 |         err2 `shouldEndWith` "Invalid S3 URL: bucket not provided"
64 | 
65 | serviceEndpoint :: S3URL -> Endpoint
66 | serviceEndpoint url = _svcEndpoint (s3Service url) NorthVirginia
67 | 
68 | withDecoded :: String -> (S3URL -> Expectation) -> Expectation
69 | withDecoded str ex = either failure ex decoded
70 | 
71 |   where
72 |     decoded = eitherDecode $ C8.pack $ "\"" <> str <> "\""
73 | 
74 |     failure err = expectationFailure $ unlines
75 |         [ "Expected " <> str <> " to parse as JSON"
76 |         , "Error: " <> err
77 |         ]
78 | 


--------------------------------------------------------------------------------
/test/Spec.hs:
--------------------------------------------------------------------------------
1 | {-# OPTIONS_GHC -F -pgmF hspec-discover #-}
2 | 


--------------------------------------------------------------------------------
/test/SpecHelper.hs:
--------------------------------------------------------------------------------
  1 | module SpecHelper
  2 |     ( module SpecHelper
  3 |     , module X
  4 |     ) where
  5 | 
  6 | import Application (makeFoundation, makeLogWare)
  7 | 
  8 | import Database.Persist.Sql
  9 |     ( SqlBackend
 10 |     , SqlPersistM
 11 |     , connEscapeName
 12 |     , rawExecute
 13 |     , rawSql
 14 |     , runSqlPersistMPool
 15 |     , unSingle
 16 |     )
 17 | import LoadEnv (loadEnvFrom)
 18 | import Yesod.Core.Handler (RedirectUrl)
 19 | import Yesod.Default.Config2 (loadYamlSettings, useEnv)
 20 | 
 21 | import Foundation as X
 22 | import Token as X
 23 | import Model as X
 24 | 
 25 | import ClassyPrelude as X hiding
 26 |     ( Handler
 27 |     , delete
 28 |     , deleteBy
 29 |     )
 30 | import Data.Aeson as X
 31 | import Database.Persist as X hiding (get, delete)
 32 | import Network.HTTP.Types as X
 33 | import Network.Wai.Test as X (SResponse(..))
 34 | import Test.Hspec as X hiding
 35 |     ( expectationFailure
 36 |     , shouldBe
 37 |     , shouldContain
 38 |     , shouldEndWith
 39 |     , shouldMatchList
 40 |     , shouldNotBe
 41 |     , shouldNotContain
 42 |     , shouldNotReturn
 43 |     , shouldNotSatisfy
 44 |     , shouldReturn
 45 |     , shouldSatisfy
 46 |     , shouldStartWith
 47 |     )
 48 | import Test.Hspec.Expectations.Lifted as X
 49 | import Text.Shakespeare.Text as X (st)
 50 | import Yesod.Persist as X (getBy404)
 51 | import Yesod.Test as X
 52 | 
 53 | withApp :: SpecWith (TestApp App) -> Spec
 54 | withApp = before $ do
 55 |     loadEnvFrom ".env.test"
 56 | 
 57 |     settings <- loadYamlSettings
 58 |         ["config/settings.yml"]
 59 |         []
 60 |         useEnv
 61 | 
 62 |     app <- makeFoundation settings
 63 |     wipeDB app
 64 |     logWare <- liftIO $ makeLogWare app
 65 |     return (app, logWare)
 66 | 
 67 | runDB :: SqlPersistM a -> YesodExample App a
 68 | runDB query = do
 69 |     app <- getTestYesod
 70 |     liftIO $ runDBWithApp app query
 71 | 
 72 | runDBWithApp :: App -> SqlPersistM a -> IO a
 73 | runDBWithApp app query = runSqlPersistMPool query (appConnPool app)
 74 | 
 75 | delete :: RedirectUrl App url => url -> YesodExample App ()
 76 | delete url = request $ do
 77 |     setMethod "DELETE"
 78 |     setUrl url
 79 | 
 80 | postJSON :: (RedirectUrl App url, ToJSON a) => url -> a -> YesodExample App ()
 81 | postJSON = requestJSON "POST"
 82 | 
 83 | patchJSON :: (RedirectUrl App url, ToJSON a) => url -> a -> YesodExample App ()
 84 | patchJSON = requestJSON "PATCH"
 85 | 
 86 | putJSON :: (RedirectUrl App url, ToJSON a) => url -> a -> YesodExample App ()
 87 | putJSON = requestJSON "PUT"
 88 | 
 89 | requestJSON :: (RedirectUrl App url, ToJSON a) => Method -> url -> a -> YesodExample App ()
 90 | requestJSON method url body = request $ do
 91 |     setMethod method
 92 |     addRequestHeader (hAccept, "application/json")
 93 |     addRequestHeader (hContentType, "application/json")
 94 |     setRequestBody $ encode body
 95 |     setUrl url
 96 | 
 97 | withJSONResponse :: FromJSON a => (a -> YesodExample App ()) -> YesodExample App ()
 98 | withJSONResponse f = withResponse $ \rsp -> do
 99 |     let bs = simpleBody rsp
100 | 
101 |     maybe (expectationFailure $ "failed to parse " <> show bs) f $ decode bs
102 | 
103 | wipeDB :: App -> IO ()
104 | wipeDB app = runDBWithApp app $ do
105 |     tables <- getTables
106 |     sqlBackend <- ask
107 | 
108 |     let escapedTables = map (connEscapeName sqlBackend . DBName) tables
109 |         query = "TRUNCATE TABLE " ++ intercalate ", " escapedTables
110 |     rawExecute query []
111 | 
112 | getTables :: MonadIO m => ReaderT SqlBackend m [Text]
113 | getTables = do
114 |     tables <- rawSql [st|
115 |         SELECT table_name
116 |         FROM information_schema.tables
117 |         WHERE table_schema = 'public';
118 |     |] []
119 | 
120 |     return $ map unSingle tables
121 | 


--------------------------------------------------------------------------------
/test/WorkerSpec.hs:
--------------------------------------------------------------------------------
 1 | module WorkerSpec
 2 |     ( main
 3 |     , spec
 4 |     ) where
 5 | 
 6 | import SpecHelper
 7 | import Worker
 8 | import Data.Time.Duration
 9 | 
10 | main :: IO ()
11 | main = hspec spec
12 | 
13 | spec :: Spec
14 | spec = withApp $
15 |     describe "archivableCommands" $ do
16 |         it "does not find recent commands" $ do
17 |             now <- liftIO getCurrentTime
18 |             token <- newToken
19 |             void $ runDB $ insert Command
20 |                 { commandToken = token
21 |                 , commandDescription = Nothing
22 |                 , commandCreatedAt = now
23 |                 }
24 | 
25 |             results <- runDB $ archivableCommands 30
26 |             results `shouldBe` []
27 | 
28 |         it "does not find old commands with recent output" $ do
29 |             now <- liftIO getCurrentTime
30 |             token <- newToken
31 |             runDB $ do
32 |                 commandId <- insert Command
33 |                     { commandToken = token
34 |                     , commandDescription = Nothing
35 |                     , commandCreatedAt = (35 :: Second) `priorTo` now
36 |                     }
37 | 
38 |                 void $ insert Output
39 |                     { outputCommand = commandId
40 |                     , outputContent = ""
41 |                     , outputCreatedAt = now
42 |                     }
43 | 
44 |             results <- runDB $ archivableCommands 30
45 |             results `shouldBe` []
46 | 
47 |         it "finds old commands with old or no output" $ do
48 |             now <- liftIO getCurrentTime
49 |             token1 <- newToken
50 |             token2 <- newToken
51 |             runDB $ do
52 |                 void $ insert Command
53 |                     { commandToken = token1
54 |                     , commandDescription = Nothing
55 |                     , commandCreatedAt = (45 :: Second) `priorTo` now
56 |                     }
57 | 
58 |                 commandId <- insert Command
59 |                     { commandToken = token2
60 |                     , commandDescription = Nothing
61 |                     , commandCreatedAt = (40 :: Second) `priorTo` now
62 |                     }
63 | 
64 |                 void $ insert Output
65 |                     { outputCommand = commandId
66 |                     , outputContent = ""
67 |                     , outputCreatedAt = (35 :: Second) `priorTo` now
68 |                     }
69 | 
70 |             results <- runDB $ archivableCommands 30
71 | 
72 |             length results `shouldBe` 2
73 | 


--------------------------------------------------------------------------------