{"id":13484049,"url":"https://github.com/choonkeat/attache","last_synced_at":"2025-04-05T07:03:45.572Z","repository":{"id":27129182,"uuid":"30597621","full_name":"choonkeat/attache","owner":"choonkeat","description":"Yet another approach to file upload","archived":false,"fork":false,"pushed_at":"2020-07-28T04:00:26.000Z","size":393,"stargazers_count":203,"open_issues_count":9,"forks_count":10,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-03-29T06:03:46.473Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/choonkeat.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-02-10T15:07:47.000Z","updated_at":"2024-02-15T15:40:04.000Z","dependencies_parsed_at":"2022-09-01T19:40:13.637Z","dependency_job_id":null,"html_url":"https://github.com/choonkeat/attache","commit_stats":null,"previous_names":[],"tags_count":17,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/choonkeat%2Fattache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/choonkeat%2Fattache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/choonkeat%2Fattache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/choonkeat%2Fattache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/choonkeat","download_url":"https://codeload.github.com/choonkeat/attache/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247299831,"owners_count":20916190,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T17:01:18.639Z","updated_at":"2025-04-05T07:03:45.544Z","avatar_url":"https://github.com/choonkeat.png","language":"Ruby","readme":"# attache\n\n[![Gem Version](https://badge.fury.io/rb/attache.svg)](https://badge.fury.io/rb/attache)\n[![Build Status](https://travis-ci.org/choonkeat/attache.svg?branch=master)](https://travis-ci.org/choonkeat/attache)\n\n## But why?\n\nIf you're interested in the \"why\", checkout [my slides](http://www.slideshare.net/choonkeat/file-upload-2015) and [the blog post](http://blog.choonkeat.com/weblog/2015/10/file-uploads-2015.html).\n\nYour app can easily support\n- dynamic resize of images (no predefined styles in your app)\n- all file types since attache let apps [display non-image files as icons through `\u003cimg src...\u003e`](https://github.com/choonkeat/attache/pull/28)\n- resumeable upload over unreliable (mobile) networks [using TUS protocol](https://github.com/choonkeat/attache/pull/10)\n\n## Run an instance\n\n#### Heroku\n\nYou can run your own instance on your own Heroku server\n\n[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)\n\n#### Docker\n\n```\ndocker run -it -p 9292:5000 --rm attache/attache\n```\n\nAlso, see [Deploying Attache on Digital Ocean using Docker](https://github.com/choonkeat/attache/wiki/Deploying-Attache-on-Digital-Ocean-using-Docker)\n\n#### RubyGem\n\nYou can install the gem and then execute `attache` command\n\n```\ngem install attache\nattache start -c web=1 -p 9292\n```\n\nNOTE: some config files will be written into your current directory\n\n```\n.\n├── Procfile\n├── config\n│   ├── puma.rb\n│   └── vhost.yml\n└── config.ru\n```\n\n#### Bundler\n\nYou can also use bundler to manage the gem; add this into your `Gemfile`\n\n```\ngem 'attache'\n```\n\nthen execute\n\n```\nbundle install\nbundle exec attache start -c web=1 -p 9292\n```\n\nNOTE: some config files will be written into your current directory (see RubyGems above)\n\n#### Source code\n\nYou can checkout the source code and run it like a regular [a Procfile-based app](https://ddollar.github.io/foreman/):\n\n```\ngit clone https://github.com/choonkeat/attache.git\ncd attache\nbundle install\nforeman start -c web=1 -p 9292\n```\n\nSee [foreman](https://github.com/ddollar/foreman) for more details.\n\n## Configuration\n\n`LOCAL_DIR` is where your local disk cache will be. By default, attache will use a system assigned temporary directory which may not be the same everytime you run attache.\n\n`CACHE_SIZE_BYTES` determines how much disk space will be used for the local disk cache. If the size of cache exceeds, least recently used files will be evicted after `CACHE_EVICTION_INTERVAL_SECONDS` duration.\n\n#### Asynchronous delete\n\nBy default `attache` will delete files from cloud storage using the lightweight, async processing library [sucker_punch](https://github.com/brandonhilkert/sucker_punch). This requires no additional setup (read: 1x free dyno).\n\nHowever if you prefer a more durable queue for reliable uploads, configuring `REDIS_PROVIDER` or `REDIS_URL` will switch `attache` to use a `redis` queue instead, via `sidekiq`. [Read Sidekiq's documentation](https://github.com/mperham/sidekiq/wiki/Using-Redis#using-an-env-variable) for details on these variables.\n\nIf for some reason you'd want the cloud storage delete to be synchronous, set `INLINE_JOB=1` instead.\n\n#### Virtual Host Cloud Storage\n\n`attache` uses a different config (and backup files into a different cloud service) depending on the request hostname that it was accessed by.\n\nThis means a single attache server can be the workhorse for different apps. Refer to `config/vhost.example.yml` file for configuration details.\n\nAt boot time, `attache` server will first look at `VHOST` environment variable. If that is missing, it will load the content of `config/vhost.yml`. If neither exist, the `attache` server run in development mode; uploaded files are only stored locally and may be evicted to free up disk space.\n\nIf you do not want to write down sensitive information like aws access key and secrets into a `config/vhost.yml` file, you can convert the entire content into `json` format and assign it to the `VHOST` environment variable instead.\n\n```\n# bash\nexport VHOST=$(bundle exec rake attache:vhost)\n\n# heroku\nheroku config:set VHOST=$(bundle exec rake attache:vhost)\n```\n\n#### Virtual Host Authorization\n\nBy default `attache` will accept uploads and delete requests from any client. Set `SECRET_KEY` to ensure attache only receives upload (and delete commands) from your own app.\n\nTo most app developers *using* attache in your rails app through a library like [attache-rails gem](https://github.com/choonkeat/attache-rails), how this work may not matter. But if you are developing attache itself or writing a client library for attache, then read on.\n\n#### Virtual Host Authorization (Developer)\n\nWhen `SECRET_KEY` is set, `attache` will require a valid `hmac` parameter in the upload request. Upload and Delete requests will be refused with `HTTP 401` error unless the `hmac` is correct.\n\nThe additional parameters required for authorized request are:\n\n* `uuid` is a uuid string\n* `expiration` is a unix timestamp of a future time. the significance is, if the timestamp has passed, the upload will be regarded as invalid\n* `hmac` is the `HMAC-SHA1` of the `SECRET_KEY` and the concatenated value of `uuid` and `expiration`\n\ni.e.\n\n``` ruby\nhmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), SECRET_KEY, uuid + expiration)\n```\n\n## APIs\n\nThe attache server is a reference implementation of these interfaces. If you write your own server, [compatibility can be verified by running a test suite](https://github.com/choonkeat/attache_api#testing-against-an-attache-compatible-server).\n\n#### Upload\n\nUsers will upload files directly into the `attache` server from their browser, bypassing the main app.\n\n\u003e ```\n\u003e PUT /upload?file=image123.jpg\n\u003e ```\n\u003e file content is the http request body\n\nThe main app front end will receive a unique `path` for each uploaded file - the only information to store in the main app database.\n\n\u003e ```\n\u003e {\"path\":\"pre/fix/image123.jpg\",\"content_type\":\"image/jpeg\",\"geometry\":\"1920x1080\"}\n\u003e ```\n\u003e json response from attache after upload.\n\n##### Upload by url\n\n\u003e ```\n\u003e GET /upload_url?url=https://example.com/logo.png\n\u003e ```\n\nAttache will download the file from `url` supplied and uploads it through the regular `/upload` handler. So be expecting the same json response after upload. works with `GET`, `POST`, `PUT`.\n\nData URIs (aka base64 encoded file binaries) can also be uploaded to the same `/upload_url` endpoint through the same `url` parameter.\n\n#### Download\n\nWhenever the main app wants to display the uploaded file, constrained to a particular size, it will use a helper method provided by the `attache` lib. e.g. `embed_attache(path)` which will generate the necessary, barebones markup.\n\n\u003e ```\n\u003e \u003cimg src=\"https://example.com/view/pre/fix/100x100/image123.jpg\" /\u003e\n\u003e ```\n\u003e use [the imagemagick resize syntax](http://www.imagemagick.org/Usage/resize/) to specify the desired output.\n\u003e\n\u003e make sure to `escape` the geometry string.\n\u003e e.g. for a hard crop of `50x50#`, the url should be `50x50%23`\n\u003e\n\u003e ```\n\u003e \u003cimg src=\"https://example.com/view/pre/fix/50x50%23/image123.jpg\" /\u003e\n\u003e ```\n\u003e requesting for a geometry of `original` will return the uploaded file. this works well for non-image file uploads.\n\u003e requesting for a geometry of `remote` will skip the local cache and serve from cloud storage.\n\n* Attache keeps the uploaded file in the local harddisk (a temp directory)\n* Attache will also upload the file into cloud storage if `FOG_CONFIG` is set\n* If the local file does not exist for some reason (e.g. cleared cache), it will download from cloud storage and store it locally\n* When a specific size is requested, it will generate the resized file based on the local file and serve it in the http response\n* If cloud storage is defined, local disk cache will store up to a maximum of `CACHE_SIZE_BYTES` bytes. By default `CACHE_SIZE_BYTES` will 80% of available diskspace\n\n#### Delete\n\n\u003e ```\n\u003e DELETE /delete\n\u003e paths=image1.jpg%0Aprefix2%2Fimage2.jpg%0Aimage3.jpg\n\u003e ```\n\nRemoving 1 or more files from the local cache and remote storage can be done via a http `POST` or `DELETE` request to `/delete`, with a `paths` parameter in the request body.\n\nThe `paths` value should be delimited by the newline character, aka `\\n`. In the example above, 3 files will be requested for deletion: `image1.jpg`, `prefix2/image2.jpg`, and `image3.jpg`.\n\n#### Backup\n\n\u003e ```\n\u003e POST /backup\n\u003e paths=image1.jpg%0Aprefix2%2Fimage2.jpg%0Aimage3.jpg\n\u003e ```\n\nThis feature might be known as `promote` in other file upload solutions. `attache` allows client app to `backup` uploaded images to another bucket for longer term storage.\n\nCopying 1 or more files from the default remote storage to the backup remote storage (backup) can be done via a http `POST` request to `/backup`, with a `paths` parameter in the request body.\n\nThe `paths` value should be delimited by the newline character, aka `\\n`. In the example above, 3 files will be requested for backup: `image1.jpg`, `prefix2/image2.jpg`, and `image3.jpg`.\n\nIf backup remote storage is not configured, this API call will be a noop. If configured, the backup storage must be accessible by the same credentials as default cloud storage as the system. Please refer to the `BACKUP_CONFIG` configuration illustrated in `config/vhost.example.yml` file in this repository.\n\nBy default, `backup` operation is performed synchronously. Set `BACKUP_ASYNC` environment variable to make it follow the same synchronicity as `delete`\n\nThe main reason to configure a backup storage is to make the default cloud storage auto expire files; mitigating [abuse](https://github.com/choonkeat/attache/issues/13). You should consult the documentation of your cloud storage provider on how to setup auto expiry, e.g. [here](https://aws.amazon.com/blogs/aws/amazon-s3-object-expiration/) or [here](https://cloud.google.com/storage/docs/lifecycle)\n\n## License\n\nMIT\n","funding_links":[],"categories":["File Upload","Ruby","File Uploading"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchoonkeat%2Fattache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchoonkeat%2Fattache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchoonkeat%2Fattache/lists"}