├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── enhancement.md │ └── question-help-wanted.md └── workflows │ ├── api.yaml │ ├── frontend.yaml │ ├── publish.yaml │ └── worker.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── api ├── .gitignore ├── .golangci.yaml ├── Dockerfile ├── Makefile ├── README.md ├── cmd │ └── iris │ │ └── api │ │ └── main.go ├── go.mod ├── go.sum ├── gqlgen.yml ├── internal │ ├── config │ │ └── config.go │ ├── graph │ │ ├── generated │ │ │ └── generated.go │ │ └── resolvers │ │ │ ├── album.resolvers.go │ │ │ ├── entity.resolvers.go │ │ │ ├── mediaitem.resolvers.go │ │ │ ├── resolver.go │ │ │ └── schema.resolvers.go │ ├── models │ │ ├── album.go │ │ ├── entity.go │ │ ├── mediaitem.go │ │ └── models_gen.go │ └── utils │ │ ├── cdn.go │ │ └── entity.go ├── pkg │ ├── mongo │ │ └── mongo.go │ └── rabbitmq │ │ └── rabbitmq.go └── schema │ ├── album.graphql │ ├── entity.graphql │ ├── mediaitem.graphql │ └── schema.graphql ├── docker-compose.yaml ├── docs ├── docs │ ├── about │ │ ├── contact.md │ │ ├── license.md │ │ └── team.md │ ├── contribution │ │ ├── api.md │ │ ├── architecture.md │ │ ├── development.md │ │ ├── introduction.md │ │ ├── user_interface.md │ │ └── worker.md │ ├── features │ │ └── introduction.md │ ├── index.md │ └── setup │ │ └── introduction.md ├── mkdocs.yml └── requirements.txt ├── frontend ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── README.md ├── nginx.conf ├── package-lock.json ├── package.json ├── public │ ├── 404.svg │ ├── albums.svg │ ├── empty_album.svg │ ├── explore.svg │ ├── favicon.ico │ ├── favourites.svg │ ├── images.svg │ ├── index.html │ ├── pytorch2021.png │ └── warning.svg └── src │ ├── App.js │ ├── App.scss │ ├── App.test.js │ ├── components │ ├── Content.js │ ├── DeleteAction.js │ ├── DeleteAlbumDialog.js │ ├── EditAlbum.js │ ├── Error.js │ ├── FavouriteAction.js │ ├── Loading.js │ ├── PeopleList.js │ ├── SideNav.js │ ├── UpdateAlbum.js │ ├── UploadDialog.js │ ├── explore │ │ ├── ExploreEntity.js │ │ └── ExploreEntityList.js │ ├── header │ │ ├── CreateAlbum.js │ │ ├── Header.js │ │ ├── SearchBar.js │ │ └── Upload.js │ └── index.js │ ├── index.js │ ├── pages │ ├── Album.js │ ├── Albums.js │ ├── Favourites.js │ ├── PageNotFound.js │ ├── Photo.js │ ├── Photos.js │ ├── Search.js │ ├── Sharing.js │ ├── Trash.js │ ├── Upcoming.js │ ├── explore │ │ ├── Entity.js │ │ ├── Explore.js │ │ ├── People.js │ │ ├── Places.js │ │ ├── Things.js │ │ └── index.js │ └── index.js │ ├── reportWebVitals.js │ ├── setupTests.js │ └── utils │ ├── capEntityName.js │ ├── index.js │ ├── reducePhotos.js │ └── sortPhotos.js ├── images ├── iris.jpeg └── logo.png ├── infra ├── mongodb │ └── create_indexes.js └── rabbitmq │ ├── definitions.json │ └── rabbitmq.config ├── ml ├── README.md └── things │ ├── efficientnet.py │ ├── imagenet_classes.txt │ └── resnet.py ├── readthedocs.yaml ├── tests ├── .gitignore ├── .pylintrc ├── behave.ini ├── data │ └── mediaitem │ │ ├── images │ │ ├── sample.gif │ │ ├── sample.heic │ │ ├── sample.jpeg │ │ ├── sample.png │ │ └── sample.webp │ │ └── metadata │ │ ├── sample.gif.json │ │ ├── sample.heic.json │ │ ├── sample.jpeg.json │ │ ├── sample.png.json │ │ └── sample.webp.json ├── features │ ├── album.feature │ ├── environment.py │ ├── explore.feature │ ├── mediaitem.feature │ └── steps │ │ ├── album.py │ │ ├── common.py │ │ └── mediaitem.py ├── helpers │ ├── __init__.py │ ├── album.py │ ├── common.py │ └── mediaitem.py └── requirements.txt └── worker ├── .gitignore ├── .pylintrc ├── Dockerfile ├── README.md ├── requirements.txt ├── scripts └── download_models.sh └── src ├── app.py ├── components ├── __init__.py ├── metadata.py ├── people.py ├── pipeline.py ├── places.py └── things.py ├── services ├── __init__.py └── rabbitmq.py └── utils ├── __init__.py ├── cdn.py └── events.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [prabhuomkar] 2 | custom: ["https://www.paypal.me/prabhuomkar", omkar.xyz] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve and fix a bug 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Short description** 11 | Description of the bug. 12 | 13 | **Environment information** 14 | * Operating System: 15 | * Golang version: 16 | * Python version: 17 | * Node version: 18 | * Docker version: 19 | * `iris` version: 20 | 21 | **How to reproduce this bug?** 22 | Give steps and screenshots if necessary explaining how you reached here 23 | 24 | OR 25 | 26 | ``` 27 | 28 | ``` 29 | 30 | **Link to logs** 31 | If applicable, 32 | 33 | **Expected behavior** 34 | What you expected to happen. 35 | 36 | **Additional context** 37 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for iris 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Example: I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question-help-wanted.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question or Help Wanted 3 | about: Ask a question or ask for some help 4 | title: '' 5 | labels: question, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What I need help with / What I was wondering** 11 | Your question, or a clear description of what you're looking for help with. 12 | 13 | **What I've tried so far** 14 | A description of what you've tried so far to solve your problem. 15 | 16 | **It would be nice if...** 17 | Could we have done anything to make things better (documentation, etc.)? 18 | 19 | **Environment information** 20 | * Operating System: 21 | * Golang version: 22 | * Python version: 23 | * Node version: 24 | * Docker version: 25 | * `iris` version: -------------------------------------------------------------------------------- /.github/workflows/api.yaml: -------------------------------------------------------------------------------- 1 | name: API CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - api/** 8 | pull_request: 9 | branches: 10 | - master 11 | paths: 12 | - api/** 13 | jobs: 14 | ci: 15 | name: Integration Check 16 | runs-on: ubuntu-latest 17 | defaults: 18 | run: 19 | working-directory: ./api 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-go@v2 23 | with: 24 | go-version: '^1.14' 25 | - run: go version 26 | - uses: golangci/golangci-lint-action@v2 27 | with: 28 | version: latest 29 | working-directory: api 30 | skip-build-cache: true 31 | - run: make build 32 | -------------------------------------------------------------------------------- /.github/workflows/frontend.yaml: -------------------------------------------------------------------------------- 1 | name: Frontend CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - frontend/** 8 | pull_request: 9 | branches: 10 | - master 11 | paths: 12 | - frontend/** 13 | jobs: 14 | ci: 15 | name: Integration Check 16 | runs-on: ubuntu-latest 17 | defaults: 18 | run: 19 | working-directory: ./frontend 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-node@v1 23 | with: 24 | node-version: 14.x 25 | - run: npm -v 26 | - run: npm ci 27 | - run: npm run lint 28 | - run: npm test 29 | - run: npm run build 30 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Images 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | push_to_registry: 7 | name: Publish Docker Images 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out repository 11 | uses: actions/checkout@v2 12 | - name: Login Docker Hub 13 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 14 | with: 15 | username: ${{ secrets.DOCKER_USERNAME }} 16 | password: ${{ secrets.DOCKER_PASSWORD }} 17 | - name: Extract metadata for Docker 18 | id: meta 19 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 20 | with: 21 | images: prabhuomkar/iris 22 | - name: Build and Push Skim Frontend 23 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 24 | with: 25 | context: ./frontend/Dockerfile 26 | push: true 27 | tags: ${{ steps.meta.outputs.tags }} 28 | labels: ${{ steps.meta.outputs.labels }} 29 | - name: Build and Push Skim API 30 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 31 | with: 32 | context: ./api/Dockerfile 33 | push: true 34 | tags: ${{ steps.meta.outputs.tags }} 35 | labels: ${{ steps.meta.outputs.labels }} 36 | - name: Build and Push Skim Worker 37 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 38 | with: 39 | context: ./worker/Dockerfile 40 | push: true 41 | tags: ${{ steps.meta.outputs.tags }} 42 | labels: ${{ steps.meta.outputs.labels }} 43 | -------------------------------------------------------------------------------- /.github/workflows/worker.yaml: -------------------------------------------------------------------------------- 1 | name: Worker CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - worker/** 8 | pull_request: 9 | branches: 10 | - master 11 | paths: 12 | - worker/** 13 | jobs: 14 | ci: 15 | name: Integration Check 16 | runs-on: ubuntu-latest 17 | defaults: 18 | run: 19 | working-directory: ./worker 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-python@v2 23 | with: 24 | python-version: '3.9' 25 | - run: python -c "import sys; print(sys.version)" 26 | - run: pip install -r requirements.txt 27 | - run: | 28 | pip install pylint 29 | pylint ./src/* 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.toml 3 | deploy/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | prabhuomkar@pm.me. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | TBD 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | start-infra: 2 | docker-compose up -d --build database queue cdn-master cdn-volume 3 | start: start-infra 4 | docker-compose up -d --build api worker frontend 5 | stop: 6 | docker-compose down 7 | test-dependencies: 8 | cd tests/ && pip install -r requirements.txt 9 | test: 10 | cd tests/ && behave -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | iris 3 | iris 4 |

5 | Open Source Photos Platform Powered by PyTorch 6 | 7 | **NOTE: This project is currently at its ALPHA stage, aiming for BETA Release in MAY 2022** 8 | 9 | ## Services 10 | **Application Services:** 11 | - [Frontend](frontend/README.md) [![Frontend](https://github.com/prabhuomkar/iris/actions/workflows/frontend.yaml/badge.svg)](https://github.com/prabhuomkar/iris/actions/workflows/frontend.yaml) 12 | - [API](api/README.md) [![API](https://github.com/prabhuomkar/iris/actions/workflows/api.yaml/badge.svg)](https://github.com/prabhuomkar/iris/actions/workflows/api.yaml) 13 | - [Worker](worker/README.md) [![Worker](https://github.com/prabhuomkar/iris/actions/workflows/worker.yaml/badge.svg)](https://github.com/prabhuomkar/iris/actions/workflows/worker.yaml) 14 | 15 | **Infrastructure Services:** 16 | - [Database: MongoDB](https://www.mongodb.com) 17 | - [CDN: SeaweedFS](http://github.com/chrislusf/seaweedfs) 18 | - [Queue: RabbitMQ](https://www.rabbitmq.com) 19 | 20 | ## Roadmap & Issues 21 | You can find the roadmap for this project [here](https://github.com/prabhuomkar/iris/projects). Issues are managed via GitHub Issues [here](https://github.com/prabhuomkar/iris/issues). 22 | 23 | ## Contributions 24 | iris is open to contributions, but if you plan to contribute new features, utility functions, or refactoring to the core, please first open an issue and discuss it with us. Sending a PR without discussion might end up resulting in a rejected PR. 25 | Please read [Contributing Guide](CONTRIBUTING.md) for details on how to contribute to this project. 26 | 27 | ## Contributors 28 | [![Contributors](https://badges.pufler.dev/contributors/prabhuomkar/iris?size=50&padding=4&bots=true)](https://github.com/prabhuomkar/iris/graphs/contributors) 29 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # Golang 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.test 8 | *.out 9 | 10 | # Others 11 | .DS_Store 12 | cmd/iris/api/api 13 | -------------------------------------------------------------------------------- /api/.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | skip-dirs: 3 | - internal/graph/generated 4 | linters: 5 | enable-all: true 6 | disable: 7 | - whitespace 8 | - godot 9 | - wrapcheck 10 | - gofumpt 11 | - gci 12 | - exhaustivestruct 13 | - dupl 14 | - funlen 15 | - cyclop 16 | - varnamelen 17 | - ireturn 18 | - nilnil 19 | linters-settings: 20 | lll: 21 | line-length: 140 22 | tab-width: 2 23 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-alpine AS builder 2 | WORKDIR /app 3 | COPY go.mod . 4 | COPY go.sum . 5 | RUN go mod download 6 | COPY . . 7 | RUN go build -o iris-api ./cmd/iris/api 8 | 9 | FROM alpine 10 | LABEL author="Omkar Prabhu" 11 | WORKDIR /app 12 | COPY --from=builder /app/iris-api . 13 | EXPOSE 5001 14 | CMD ["/app/iris-api"] -------------------------------------------------------------------------------- /api/Makefile: -------------------------------------------------------------------------------- 1 | all: code-quality 2 | 3 | lint: 4 | golangci-lint run ./... 5 | 6 | generate: 7 | go run github.com/99designs/gqlgen generate 8 | 9 | build: 10 | go build -o cmd/iris/api/api cmd/iris/api/main.go 11 | 12 | run: build 13 | ./cmd/iris/api/api 14 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # api 2 | 3 | ## Getting Started 4 | 5 | ### Prerequisites 6 | Following are the softwares requried to get everything up and running. 7 | - [Docker](https://docs.docker.com/engine/install/) for Infrastructure Components 8 | - [Go](https://golang.org/dl/) for API 9 | 10 | ### Installing 11 | **For Local Setup** 12 | - Add the below line in your `/etc/hosts` 13 | ``` 14 | 127.0.0.1 cdn-master cdn-volume database api queue frontend worker ml 15 | ``` 16 | - Build and start the containers 17 | ``` 18 | docker-compose up -d 19 | ``` 20 | 21 | **For API** 22 | - Install go modules 23 | ``` 24 | go mod download 25 | ``` 26 | - Start the API 27 | ``` 28 | make run 29 | ``` 30 | - Linting the code 31 | ``` 32 | make lint 33 | ``` 34 | 35 | ### Configuration 36 | TBD 37 | -------------------------------------------------------------------------------- /api/cmd/iris/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "iris/api/internal/config" 15 | "iris/api/internal/graph/generated" 16 | "iris/api/internal/graph/resolvers" 17 | "iris/api/pkg/mongo" 18 | "iris/api/pkg/rabbitmq" 19 | 20 | "github.com/99designs/gqlgen/graphql/handler" 21 | "github.com/99designs/gqlgen/graphql/handler/extension" 22 | "github.com/99designs/gqlgen/graphql/handler/transport" 23 | "github.com/99designs/gqlgen/graphql/playground" 24 | "github.com/go-chi/chi" 25 | "github.com/linxGnu/goseaweedfs" 26 | "github.com/rs/cors" 27 | ) 28 | 29 | const shutdownTime = 10 30 | 31 | func main() { 32 | // handle graceful shutdown 33 | interrupt := make(chan os.Signal, 1) 34 | signal.Notify(interrupt, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 35 | 36 | // initialize config 37 | cfg, err := config.Init() 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | db, err := mongo.Init(cfg.Database.URI, cfg.Database.Name) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | queue, err := rabbitmq.Init(cfg.Queue.URI, cfg.Queue.Name) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | seaweed, err := goseaweedfs.NewSeaweed( 53 | cfg.CDN.URL, nil, cfg.CDN.ChunkSize, 54 | &http.Client{Timeout: time.Duration(cfg.CDN.Timeout) * time.Minute}) 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | router := chi.NewRouter() 60 | router.Use(cors.New(cors.Options{ 61 | AllowCredentials: true, 62 | AllowedMethods: []string{http.MethodPost, http.MethodGet, http.MethodOptions, http.MethodOptions}, 63 | AllowedHeaders: []string{"*"}, 64 | }).Handler) 65 | 66 | c := generated.Config{ 67 | Resolvers: &resolvers.Resolver{ 68 | Config: &cfg, 69 | DB: db, 70 | Queue: queue, 71 | CDN: seaweed, 72 | }, 73 | } 74 | 75 | gqlHandler := handler.New(generated.NewExecutableSchema(c)) 76 | gqlHandler.AddTransport(transport.Options{}) 77 | gqlHandler.AddTransport(transport.POST{}) 78 | gqlHandler.AddTransport(transport.GET{}) 79 | gqlHandler.AddTransport(transport.MultipartForm{}) 80 | gqlHandler.Use(extension.Introspection{}) 81 | 82 | router.Handle("/", playground.Handler("graphql playground", "/graphql")) 83 | router.Handle("/graphql", gqlHandler) 84 | 85 | srv := &http.Server{ 86 | Addr: fmt.Sprintf(":%d", cfg.Port), 87 | Handler: router, 88 | } 89 | 90 | go func() { 91 | err = srv.ListenAndServe() 92 | if err != nil && !errors.Is(err, http.ErrServerClosed) { 93 | log.Fatalf("error starting iris api: %+v", err) 94 | } 95 | }() 96 | log.Printf("started iris api on port: %d", cfg.Port) 97 | 98 | <-interrupt 99 | log.Print("stopping iris api") 100 | 101 | ctx, cancel := context.WithTimeout(context.Background(), shutdownTime*time.Second) 102 | defer func() { 103 | _ = db.Client.Disconnect(ctx) 104 | 105 | _ = queue.Disconnect() 106 | 107 | cancel() 108 | }() 109 | 110 | if err := srv.Shutdown(ctx); err != nil { 111 | log.Printf("error shutting down iris api: %+v", err) 112 | } 113 | 114 | log.Print("stopped iris api") 115 | } 116 | -------------------------------------------------------------------------------- /api/go.mod: -------------------------------------------------------------------------------- 1 | module iris/api 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.13.0 7 | github.com/agnivade/levenshtein v1.1.1 // indirect 8 | github.com/go-chi/chi v4.1.2+incompatible 9 | github.com/go-stack/stack v1.8.1 // indirect 10 | github.com/golang/snappy v0.0.4 // indirect 11 | github.com/kelseyhightower/envconfig v1.4.0 12 | github.com/klauspost/compress v1.13.5 // indirect 13 | github.com/linxGnu/goseaweedfs v0.1.5 14 | github.com/rs/cors v1.8.0 15 | github.com/streadway/amqp v1.0.0 16 | github.com/vektah/gqlparser/v2 v2.1.0 17 | github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect 18 | go.mongodb.org/mongo-driver v1.7.2 19 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect 20 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 21 | golang.org/x/text v0.3.7 // indirect 22 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 23 | gopkg.in/yaml.v2 v2.4.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /api/gqlgen.yml: -------------------------------------------------------------------------------- 1 | schema: 2 | - schema/*.graphql 3 | 4 | exec: 5 | filename: internal/graph/generated/generated.go 6 | package: generated 7 | 8 | model: 9 | filename: internal/models/models_gen.go 10 | package: models 11 | 12 | resolver: 13 | layout: follow-schema 14 | dir: internal/graph/resolvers 15 | package: resolvers 16 | filename_template: '{name}.resolvers.go' 17 | 18 | autobind: 19 | - 'iris/api/internal/models' 20 | 21 | models: 22 | ID: 23 | model: 24 | - github.com/99designs/gqlgen/graphql.ID 25 | - github.com/99designs/gqlgen/graphql.Int 26 | - github.com/99designs/gqlgen/graphql.Int64 27 | - github.com/99designs/gqlgen/graphql.Int32 28 | Int: 29 | model: 30 | - github.com/99designs/gqlgen/graphql.Int 31 | - github.com/99designs/gqlgen/graphql.Int64 32 | - github.com/99designs/gqlgen/graphql.Int32 -------------------------------------------------------------------------------- /api/internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/kelseyhightower/envconfig" 8 | ) 9 | 10 | const ( 11 | FeaturePeople = "people" 12 | FeaturePlaces = "places" 13 | FeatureThings = "things" 14 | ) 15 | 16 | type ( 17 | Database struct { 18 | URI string `envconfig:"DB_URI" default:"mongodb://root:root@127.0.0.1:5010/iris?authSource=admin"` 19 | Name string `enconfig:"DB_NAME" default:"iris"` 20 | } 21 | 22 | CDN struct { 23 | URL string `envconfig:"CDN_URL" default:"http://cdn-master:5020"` 24 | ChunkSize int64 `envconfig:"CDN_CHUNK_SIZE" default:"1048576"` 25 | Timeout int64 `envconfig:"CDN_TIMEOUT" default:"5"` 26 | } 27 | 28 | Queue struct { 29 | URI string `envconfig:"QUEUE_URI" default:"amqp://root:root@127.0.0.1:5030"` 30 | Name string `envconfig:"QUEUE_NAME" default:"iris.process"` 31 | } 32 | 33 | FeatureConfig struct { 34 | DisablePlaces bool `envconfig:"FEATURE_DISABLE_PLACES" default:"false"` 35 | DisablePeople bool `envconfig:"FEATURE_DISABLE_PEOPLE" default:"false"` 36 | DisableThings bool `envconfig:"FEATURE_DISABLE_THINGS" default:"false"` 37 | } 38 | 39 | Config struct { 40 | Database 41 | CDN 42 | Queue 43 | FeatureConfig 44 | Port int `envconfig:"PORT" default:"5001"` 45 | } 46 | ) 47 | 48 | func Init() (config Config, err error) { 49 | err = envconfig.Process("", &config) 50 | 51 | return 52 | } 53 | 54 | func (c Config) GetMediaItemFeatures() string { 55 | result := []string{} 56 | 57 | if !c.FeatureConfig.DisablePlaces { 58 | result = append(result, fmt.Sprintf("%q", FeaturePlaces)) 59 | } 60 | 61 | if !c.FeatureConfig.DisablePeople { 62 | result = append(result, fmt.Sprintf("%q", FeaturePeople)) 63 | } 64 | 65 | if !c.FeatureConfig.DisableThings { 66 | result = append(result, fmt.Sprintf("%q", FeatureThings)) 67 | } 68 | 69 | return strings.Join(result, ",") 70 | } 71 | -------------------------------------------------------------------------------- /api/internal/graph/resolvers/mediaitem.resolvers.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "iris/api/internal/graph/generated" 10 | "iris/api/internal/models" 11 | "log" 12 | "time" 13 | 14 | "go.mongodb.org/mongo-driver/bson" 15 | "go.mongodb.org/mongo-driver/bson/primitive" 16 | "go.mongodb.org/mongo-driver/mongo" 17 | ) 18 | 19 | func (r *mediaItemResolver) Entities(ctx context.Context, obj *models.MediaItem) ([]*models.Entity, error) { 20 | entityIDs := make([]primitive.ObjectID, len(obj.Entities)) 21 | 22 | for idx, strID := range obj.Entities { 23 | oid, _ := primitive.ObjectIDFromHex(strID) 24 | entityIDs[idx] = oid 25 | } 26 | 27 | cur, err := r.DB.Collection(models.ColEntity).Find(ctx, bson.M{"_id": bson.M{"$in": entityIDs}}) 28 | if err != nil { 29 | log.Printf("error getting mediaitem related entities: %+v", err) 30 | 31 | return nil, err 32 | } 33 | 34 | var result []*models.Entity 35 | if err = cur.All(ctx, &result); err != nil { 36 | log.Printf("error decoding mediaitem related entities: %+v", err) 37 | 38 | return nil, err 39 | } 40 | 41 | return result, nil 42 | } 43 | 44 | func (r *mediaItemResolver) Albums(ctx context.Context, obj *models.MediaItem) ([]*models.Album, error) { 45 | albumIDs := make([]primitive.ObjectID, len(obj.Albums)) 46 | 47 | for idx, strID := range obj.Albums { 48 | oid, _ := primitive.ObjectIDFromHex(strID) 49 | albumIDs[idx] = oid 50 | } 51 | 52 | cur, err := r.DB.Collection(models.ColAlbums).Find(ctx, bson.M{"_id": bson.M{"$in": albumIDs}}) 53 | if err != nil { 54 | log.Printf("error getting mediaitem related albums: %+v", err) 55 | 56 | return nil, err 57 | } 58 | 59 | var result []*models.Album 60 | if err = cur.All(ctx, &result); err != nil { 61 | log.Printf("error decoding mediaitem related albums: %+v", err) 62 | 63 | return nil, err 64 | } 65 | 66 | return result, nil 67 | } 68 | 69 | func (r *mutationResolver) UpdateDescription(ctx context.Context, id string, description string) (bool, error) { 70 | oid, err := primitive.ObjectIDFromHex(id) 71 | if err != nil { 72 | return false, err 73 | } 74 | 75 | _, err = r.DB.Collection(models.ColMediaItems).UpdateByID(ctx, oid, bson.D{ 76 | {Key: "$set", Value: bson.D{{Key: "description", Value: description}}}, 77 | }) 78 | if err != nil { 79 | log.Printf("error updating mediaitem description: %+v", err) 80 | 81 | return false, err 82 | } 83 | 84 | return true, nil 85 | } 86 | 87 | func (r *queryResolver) MediaItem(ctx context.Context, id string) (*models.MediaItem, error) { 88 | oid, err := primitive.ObjectIDFromHex(id) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | filter := bson.D{{Key: "_id", Value: oid}} 94 | 95 | var result *models.MediaItem 96 | 97 | err = r.DB.Collection(models.ColMediaItems).FindOne(ctx, filter).Decode(&result) 98 | if err != nil { 99 | if errors.Is(err, mongo.ErrNoDocuments) { 100 | return nil, err 101 | } 102 | 103 | log.Printf("error getting mediaitem: %+v", err) 104 | 105 | return nil, err 106 | } 107 | 108 | return result, err 109 | } 110 | 111 | func (r *queryResolver) MediaItems(ctx context.Context, page *int, limit *int) (*models.MediaItemConnection, error) { 112 | defaultMediaItemsLimit := 20 113 | defaultMediaItemsPage := 1 114 | 115 | if limit == nil { 116 | limit = &defaultMediaItemsLimit 117 | } 118 | 119 | if page == nil { 120 | page = &defaultMediaItemsPage 121 | } 122 | 123 | skip := int64(*limit * (*page - 1)) 124 | itemsPerPage := int64(*limit) 125 | 126 | colQuery := bson.A{ 127 | bson.D{{Key: "$match", Value: bson.D{ 128 | {Key: "deleted", Value: bson.D{{Key: "$not", Value: bson.D{{Key: "$eq", Value: true}}}}}, 129 | }}}, 130 | bson.D{{Key: "$sort", Value: bson.D{{Key: "mediaMetadata.creationTime", Value: -1}}}}, 131 | bson.D{{Key: "$skip", Value: skip}}, 132 | bson.D{{Key: "$limit", Value: itemsPerPage}}, 133 | } 134 | cntQuery := bson.A{ 135 | bson.D{{Key: "$match", Value: bson.D{ 136 | {Key: "deleted", Value: bson.D{{Key: "$not", Value: bson.D{{Key: "$eq", Value: true}}}}}, 137 | }}}, 138 | bson.D{{Key: "$count", Value: "count"}}, 139 | } 140 | facetStage := bson.D{{ 141 | Key: "$facet", 142 | Value: bson.D{{Key: "mediaItems", Value: colQuery}, {Key: "totalCount", Value: cntQuery}}, 143 | }} 144 | 145 | cur, err := r.DB.Collection(models.ColMediaItems).Aggregate(ctx, mongo.Pipeline{facetStage}) 146 | if err != nil { 147 | log.Printf("error getting mediaitems: %+v", err) 148 | 149 | return nil, err 150 | } 151 | 152 | var result []*struct { 153 | MediaItems []*models.MediaItem `bson:"mediaItems"` 154 | TotalCount []*struct { 155 | Count *int `bson:"count"` 156 | } `bson:"totalCount"` 157 | } 158 | 159 | if err = cur.All(ctx, &result); err != nil { 160 | log.Printf("error decoding mediaitems: %+v", err) 161 | 162 | return nil, err 163 | } 164 | 165 | totalCount := 0 166 | if len(result) != 0 && len(result[0].TotalCount) != 0 { 167 | totalCount = *result[0].TotalCount[0].Count 168 | } 169 | 170 | return &models.MediaItemConnection{ 171 | TotalCount: totalCount, 172 | Nodes: result[0].MediaItems, 173 | }, nil 174 | } 175 | 176 | func (r *queryResolver) OnThisDay(ctx context.Context) ([]*models.OnThisDayResponse, error) { 177 | matchStage := bson.D{{Key: "$match", Value: bson.D{{ 178 | Key: "$expr", Value: bson.D{{ 179 | Key: "$and", Value: bson.A{ 180 | bson.D{{Key: "$eq", Value: bson.A{ 181 | bson.D{{Key: "$dayOfMonth", Value: "$mediaMetadata.creationTime"}}, 182 | bson.D{{Key: "$dayOfMonth", Value: time.Now()}}, 183 | }}}, 184 | bson.D{{Key: "$eq", Value: bson.A{ 185 | bson.D{{Key: "$month", Value: "$mediaMetadata.creationTime"}}, 186 | bson.D{{Key: "$month", Value: time.Now()}}, 187 | }}}, 188 | }, 189 | }}, 190 | }}}} 191 | groupStage := bson.D{{Key: "$group", Value: bson.D{ 192 | { 193 | Key: "_id", Value: bson.D{{ 194 | Key: "year", Value: bson.D{{Key: "$year", Value: "$mediaMetadata.creationTime"}}, 195 | }}, 196 | }, 197 | { 198 | Key: "mediaItems", Value: bson.D{{ 199 | Key: "$push", Value: "$$ROOT", 200 | }}, 201 | }, 202 | }}} 203 | 204 | cur, err := r.DB.Collection(models.ColMediaItems).Aggregate(ctx, mongo.Pipeline{ 205 | matchStage, 206 | groupStage, 207 | bson.D{{Key: "$sort", Value: bson.D{{Key: "_id", Value: -1}}}}, 208 | }) 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | var result []*models.OnThisDayResponse 214 | 215 | if err = cur.All(ctx, &result); err != nil { 216 | log.Printf("error getting on this day mediaitems: %+v", err) 217 | 218 | return nil, err 219 | } 220 | 221 | return result, nil 222 | } 223 | 224 | // MediaItem returns generated.MediaItemResolver implementation. 225 | func (r *Resolver) MediaItem() generated.MediaItemResolver { return &mediaItemResolver{r} } 226 | 227 | type mediaItemResolver struct{ *Resolver } 228 | -------------------------------------------------------------------------------- /api/internal/graph/resolvers/resolver.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "iris/api/internal/config" 5 | "iris/api/pkg/mongo" 6 | "iris/api/pkg/rabbitmq" 7 | 8 | "github.com/linxGnu/goseaweedfs" 9 | ) 10 | 11 | // This file will not be regenerated automatically. 12 | // 13 | // It serves as dependency injection for your app, add any dependencies you require here. 14 | 15 | type Resolver struct { 16 | Config *config.Config 17 | DB *mongo.Connection 18 | Queue *rabbitmq.Connection 19 | CDN *goseaweedfs.Seaweed 20 | } 21 | -------------------------------------------------------------------------------- /api/internal/models/album.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | const ColAlbums = "albums" 6 | 7 | type ( 8 | Album struct { 9 | ID string `json:"id" bson:"_id"` 10 | Name string `json:"name"` 11 | Description string `json:"description"` 12 | PreviewMediaItem string `json:"previewMediaItem"` 13 | MediaItems []string `json:"mediaItems"` 14 | CreatedAt time.Time `json:"createdAt"` 15 | UpdatedAt time.Time `json:"updatedAt"` 16 | } 17 | ) 18 | -------------------------------------------------------------------------------- /api/internal/models/entity.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | const ColEntity = "entities" 4 | 5 | type Entity struct { 6 | ID string `json:"id" bson:"_id"` 7 | Name string `json:"name"` 8 | EntityType string `json:"entityType"` 9 | PreviewMediaItem string `json:"previewMediaItem"` 10 | MediaItems []string `json:"mediaItems"` 11 | CreatedAt string `json:"createdAt"` 12 | UpdatedAt string `json:"updatedAt"` 13 | } 14 | -------------------------------------------------------------------------------- /api/internal/models/mediaitem.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | const ColMediaItems = "mediaitems" 6 | 7 | type ( 8 | MediaItem struct { 9 | ID string `json:"id" bson:"_id"` 10 | Description string `json:"description"` 11 | PreviewURL string `json:"previewUrl"` 12 | SourceURL string `json:"sourceUrl"` 13 | MimeType string `json:"mimeType"` 14 | FileName string `json:"fileName"` 15 | FileSize int64 `json:"fileSize"` 16 | MediaMetadata *MediaMetaData `json:"mediaMetadata"` 17 | ContentCategories []string `json:"contentCategories"` 18 | Entities []string `json:"entities"` 19 | Albums []string `json:"albums"` 20 | Faces []Face `json:"faces"` 21 | Favourite bool `json:"favourite"` 22 | Deleted bool `json:"deleted"` 23 | CreatedAt time.Time `json:"createdAt"` 24 | UpdatedAt time.Time `json:"updatedAt"` 25 | Status string `json:"status"` 26 | } 27 | 28 | MediaMetaData struct { 29 | CreationTime time.Time `json:"creationTime"` 30 | Width *int `json:"width"` 31 | Height *int `json:"height"` 32 | Photo *Photo `json:"photo"` 33 | Video *Video `json:"video"` 34 | Location *Location `json:"location"` 35 | } 36 | 37 | Photo struct { 38 | CameraMake *string `json:"cameraMake"` 39 | CameraModel *string `json:"cameraModel"` 40 | FocalLength *float64 `json:"focalLength"` 41 | ApertureFNumber *float64 `json:"apertureFNumber"` 42 | IsoEquivalent *int `json:"isoEquivalent"` 43 | ExposureTime *float64 `json:"exposureTime"` 44 | } 45 | 46 | Video struct { 47 | CameraMake *string `json:"cameraMake"` 48 | CameraModel *string `json:"cameraModel"` 49 | Fps *int `json:"fps"` 50 | Status *string `json:"status"` 51 | } 52 | 53 | Location struct { 54 | Latitude *float64 `json:"latitude"` 55 | Longitude *float64 `json:"longitude"` 56 | } 57 | 58 | Face struct { 59 | EntityID string `json:"entityId"` 60 | PreviewURL string `json:"previewUrl"` 61 | } 62 | ) 63 | -------------------------------------------------------------------------------- /api/internal/models/models_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package models 4 | 5 | type AlbumConnection struct { 6 | Nodes []*Album `json:"nodes"` 7 | TotalCount int `json:"totalCount"` 8 | } 9 | 10 | type AutocompleteResponse struct { 11 | ID string `json:"id"` 12 | Name string `json:"name"` 13 | } 14 | 15 | type CreateAlbumInput struct { 16 | Name string `json:"name"` 17 | Description *string `json:"description"` 18 | MediaItems []*string `json:"mediaItems"` 19 | } 20 | 21 | type EntityItemConnection struct { 22 | Nodes []*Entity `json:"nodes"` 23 | TotalCount int `json:"totalCount"` 24 | } 25 | 26 | type ExploreResponse struct { 27 | People []*Entity `json:"people"` 28 | Places []*Entity `json:"places"` 29 | Things []*Entity `json:"things"` 30 | } 31 | 32 | type MediaItemConnection struct { 33 | Nodes []*MediaItem `json:"nodes"` 34 | TotalCount int `json:"totalCount"` 35 | } 36 | 37 | type OnThisDayResponse struct { 38 | Year int `json:"year"` 39 | MediaItems []*MediaItem `json:"mediaItems"` 40 | } 41 | 42 | type UpdateAlbumInput struct { 43 | Name string `json:"name"` 44 | Description *string `json:"description"` 45 | } 46 | -------------------------------------------------------------------------------- /api/internal/utils/cdn.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | 7 | "github.com/linxGnu/goseaweedfs" 8 | ) 9 | 10 | func DeleteFilesFromCDN(cdn *goseaweedfs.Seaweed, imageURLs []string) { 11 | for _, imageURL := range imageURLs { 12 | if len(imageURL) > 0 { 13 | splits := strings.Split(imageURL, "/") 14 | fileID := splits[len(splits)-1] 15 | 16 | err := cdn.DeleteFile(fileID, nil) 17 | if err != nil { 18 | log.Printf("error deleting images from cdn: %+v", err) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /api/internal/utils/entity.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "iris/api/internal/models" 6 | "iris/api/pkg/mongo" 7 | "log" 8 | 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/mongo/options" 11 | ) 12 | 13 | // Returns list of entities based on entity type 14 | func GetEntitiesByType(ctx context.Context, db *mongo.Connection, entityType string) ([]*models.Entity, error) { 15 | defaultEntityListItemLimit := int64(10) // nolint 16 | 17 | cur, err := db.Collection(models.ColEntity).Find(ctx, 18 | bson.D{{Key: "entityType", Value: entityType}}, &options.FindOptions{Limit: &defaultEntityListItemLimit}) 19 | if err != nil { 20 | log.Printf("error getting entities by type: %+v", err) 21 | 22 | return nil, err 23 | } 24 | 25 | var result []*models.Entity 26 | if err = cur.All(ctx, &result); err != nil { 27 | log.Printf("error decoding entities by type: %+v", err) 28 | 29 | return nil, err 30 | } 31 | 32 | return result, nil 33 | } 34 | -------------------------------------------------------------------------------- /api/pkg/mongo/mongo.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "go.mongodb.org/mongo-driver/mongo" 9 | "go.mongodb.org/mongo-driver/mongo/options" 10 | "go.mongodb.org/mongo-driver/mongo/readpref" 11 | ) 12 | 13 | const ( 14 | connectionCtxTimeout = 15 15 | pingCtxTimeout = 5 16 | ) 17 | 18 | // Connection maintains instance of client connection and database name 19 | type Connection struct { 20 | Client *mongo.Client 21 | Database string 22 | } 23 | 24 | // Init will initialize mongodb connection 25 | func Init(mongoURI, databaseName string) (*Connection, error) { 26 | ctx, cancel := context.WithTimeout(context.Background(), connectionCtxTimeout*time.Second) 27 | defer cancel() 28 | 29 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI)) 30 | if err != nil { 31 | log.Printf("error connecting to database mongodb: %+v", err) 32 | 33 | return nil, err 34 | } 35 | 36 | ctx, cancel = context.WithTimeout(context.Background(), pingCtxTimeout*time.Second) 37 | defer cancel() 38 | 39 | err = client.Ping(ctx, readpref.Primary()) 40 | if err != nil { 41 | log.Printf("error pinging database mongodb: %+v", err) 42 | 43 | return nil, err 44 | } 45 | 46 | log.Printf("connected to database mongodb") 47 | 48 | return &Connection{ 49 | Client: client, 50 | Database: databaseName, 51 | }, err 52 | } 53 | 54 | // Disconnect will close the existing mongodb client instance 55 | func (c *Connection) Disconnect() error { 56 | ctx, cancel := context.WithTimeout(context.Background(), connectionCtxTimeout*time.Second) 57 | defer cancel() 58 | 59 | return c.Client.Disconnect(ctx) 60 | } 61 | 62 | // Collection will return mongo collection instance 63 | func (c *Connection) Collection(name string) *mongo.Collection { 64 | return c.Client.Database(c.Database).Collection(name) 65 | } 66 | -------------------------------------------------------------------------------- /api/pkg/rabbitmq/rabbitmq.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/streadway/amqp" 7 | ) 8 | 9 | // Connection maintains instance of client connection 10 | type Connection struct { 11 | Connection *amqp.Connection 12 | Channel *amqp.Channel 13 | Queue string 14 | } 15 | 16 | // Init will initialize rabbitmq connection 17 | func Init(amqpURI, queue string) (*Connection, error) { 18 | connection, err := amqp.Dial(amqpURI) 19 | if err != nil { 20 | log.Printf("error connecting to queue rabbitmq: %+v", err) 21 | 22 | return nil, err 23 | } 24 | 25 | channel, err := connection.Channel() 26 | if err != nil { 27 | log.Printf("error opening channel of queue rabbitmq: %+v", err) 28 | 29 | return nil, err 30 | } 31 | 32 | log.Printf("connected to queue rabbitmq") 33 | 34 | return &Connection{ 35 | Connection: connection, 36 | Channel: channel, 37 | Queue: queue, 38 | }, nil 39 | } 40 | 41 | // Disconnect will close the existing rabbitmq connection instance 42 | func (c *Connection) Disconnect() error { 43 | return c.Connection.Close() 44 | } 45 | 46 | // Publish will send a message to rabbitmq exchange 47 | func (c *Connection) Publish(message []byte) error { 48 | log.Printf("publishing message to queue rabbitmq: %s", string(message)) 49 | 50 | return c.Channel.Publish( 51 | "", c.Queue, false, false, 52 | amqp.Publishing{ 53 | ContentType: "application/json", 54 | Body: message, 55 | }, 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /api/schema/album.graphql: -------------------------------------------------------------------------------- 1 | type Album { 2 | id: String! 3 | name: String! 4 | description: String 5 | previewUrl: String! 6 | mediaItems(page: Int, limit: Int): MediaItemConnection! 7 | createdAt: Time! 8 | updatedAt: Time! 9 | } 10 | 11 | type AlbumConnection { 12 | nodes: [Album!] 13 | totalCount: Int! 14 | } 15 | 16 | input CreateAlbumInput { 17 | name: String! 18 | description: String 19 | mediaItems: [String] 20 | } 21 | 22 | input UpdateAlbumInput { 23 | name: String! 24 | description: String 25 | } 26 | 27 | extend type Query { 28 | album(id: String!): Album! 29 | albums(page: Int, limit: Int, sortBy: String): AlbumConnection! 30 | } 31 | 32 | extend type Mutation { 33 | createAlbum(input: CreateAlbumInput!): String 34 | updateAlbum(id: String!, input: UpdateAlbumInput!): Boolean! 35 | updateAlbumPreviewMediaItem(id: String!, mediaItemId: String!): Boolean! 36 | deleteAlbum(id: String!): Boolean! 37 | updateAlbumMediaItems(id: String!, type: String!, mediaItems: [String!]!): Boolean! 38 | } 39 | -------------------------------------------------------------------------------- /api/schema/entity.graphql: -------------------------------------------------------------------------------- 1 | type Entity { 2 | id: String! 3 | name: String! 4 | previewUrl: String! 5 | entityType: String! 6 | mediaItems(page: Int, limit: Int): MediaItemConnection! 7 | } 8 | 9 | type EntityItemConnection { 10 | nodes: [Entity!] 11 | totalCount: Int! 12 | } 13 | 14 | type ExploreResponse { 15 | people: [Entity!] 16 | places: [Entity!] 17 | things: [Entity!] 18 | } 19 | 20 | extend type Query { 21 | explore: ExploreResponse! 22 | entities(entityType: String!, page: Int, limit: Int): EntityItemConnection! 23 | entity(id: String!): Entity! 24 | } 25 | 26 | extend type Mutation { 27 | updateEntity(id: String!, name: String!): Boolean! 28 | updateEntityPreviewMediaItem(id: String!, entityId: String!): Boolean! 29 | } 30 | -------------------------------------------------------------------------------- /api/schema/mediaitem.graphql: -------------------------------------------------------------------------------- 1 | scalar Upload 2 | 3 | type MediaItem { 4 | id: String! 5 | description: String! 6 | previewUrl: String! 7 | sourceUrl: String! 8 | mimeType: String! 9 | fileName: String! 10 | fileSize: Int! 11 | mediaMetadata: MediaMetaData 12 | contentCategories: [String] 13 | entities: [Entity!] 14 | albums: [Album!] 15 | favourite: Boolean 16 | deleted: Boolean 17 | createdAt: Time! 18 | updatedAt: Time! 19 | status: String! 20 | } 21 | 22 | type MediaMetaData { 23 | creationTime: Time 24 | width: Int 25 | height: Int 26 | photo: Photo 27 | video: Video 28 | location: Location 29 | } 30 | 31 | type Photo { 32 | cameraMake: String 33 | cameraModel: String 34 | focalLength: Float 35 | apertureFNumber: Float 36 | isoEquivalent: Int 37 | exposureTime: Float 38 | } 39 | 40 | type Video { 41 | cameraMake: String 42 | cameraModel: String 43 | fps: Int 44 | status: String 45 | } 46 | 47 | type Location { 48 | latitude: Float 49 | longitude: Float 50 | } 51 | 52 | type MediaItemConnection { 53 | nodes: [MediaItem!] 54 | totalCount: Int! 55 | } 56 | 57 | type OnThisDayResponse { 58 | year: Int! 59 | mediaItems: [MediaItem!] 60 | } 61 | 62 | extend type Query { 63 | mediaItem(id: String!): MediaItem! 64 | mediaItems(page: Int, limit: Int): MediaItemConnection! 65 | onThisDay: [OnThisDayResponse!] 66 | } 67 | 68 | extend type Mutation { 69 | updateDescription(id: String!, description: String!): Boolean! 70 | } 71 | -------------------------------------------------------------------------------- /api/schema/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar Time 2 | 3 | type AutocompleteResponse { 4 | id: String! 5 | name: String! 6 | } 7 | 8 | extend type Query { 9 | search(q: String, id: String, page: Int, limit: Int): MediaItemConnection! 10 | autocomplete(q: String!): [AutocompleteResponse] 11 | favourites(page: Int, limit: Int): MediaItemConnection! 12 | deleted(page: Int, limit: Int): MediaItemConnection! 13 | } 14 | 15 | extend type Mutation { 16 | upload(file: Upload!, albumId: String): String! 17 | favourite(id: String!, type: String!): Boolean! 18 | delete(id: String!, type: String!): Boolean! 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | # applications 4 | frontend: 5 | build: 6 | context: frontend 7 | args: 8 | REACT_APP_API_URL: http://api:5001/graphql 9 | restart: always 10 | container_name: frontend 11 | ports: 12 | - '5000:80' 13 | depends_on: 14 | - api 15 | api: 16 | build: api 17 | restart: always 18 | container_name: api 19 | ports: 20 | - '5001:5001' 21 | depends_on: 22 | - cdn-master 23 | - database 24 | - queue 25 | environment: 26 | CDN_URL: 'http://cdn-master:5020' 27 | CDN_CHUNK_SIZE: 1048576 28 | CDN_TIMEOUT: 5 29 | DB_URI: 'mongodb://root:root@database:5010/iris?authSource=admin' 30 | DB_NAME: iris 31 | QUEUE_URI: 'amqp://root:root@queue:5030' 32 | QUEUE_NAME: iris.process 33 | worker: 34 | build: worker 35 | restart: always 36 | container_name: worker 37 | environment: 38 | CDN_URL: cdn-master 39 | CDN_PORT: 5020 40 | DB_URI: mongodb://root:root@database:5010/iris?authSource=admin 41 | DB_NAME: iris 42 | QUEUE_URI: amqp://root:root@queue:5030/ 43 | INPUT_QUEUE_NAME: iris.process 44 | OUTPUT_QUEUE_NAME: iris.results 45 | depends_on: 46 | - queue 47 | # infra services 48 | database: 49 | image: mongo 50 | restart: always 51 | container_name: database 52 | environment: 53 | MONGO_INITDB_ROOT_USERNAME: root 54 | MONGO_INITDB_ROOT_PASSWORD: root 55 | MONGO_INITDB_DATABASE: iris 56 | command: 'mongod --port 5010' 57 | ports: 58 | - '5010:5010' 59 | volumes: 60 | - ./infra/mongodb/create_indexes.js:/docker-entrypoint-initdb.d/create_indexes.js:ro 61 | cdn-master: 62 | container_name: cdn-master 63 | image: chrislusf/seaweedfs 64 | ports: 65 | - '5020:5020' 66 | - '15020:15020' 67 | command: 'master -ip=cdn-master -port=5020 -port.grpc=15020' 68 | cdn-volume: 69 | container_name: cdn-volume 70 | image: chrislusf/seaweedfs 71 | ports: 72 | - '5021:5021' 73 | - '15021:15021' 74 | command: 'volume -ip=cdn-volume -mserver="cdn-master:5020" -port=5021' 75 | depends_on: 76 | - cdn-master 77 | queue: 78 | container_name: queue 79 | image: rabbitmq:3.9.5-management 80 | environment: 81 | RABBITMQ_NODE_PORT: 5030 82 | RABBITMQ_DEFAULT_USER: root 83 | RABBITMQ_DEFAULT_PASS: root 84 | ports: 85 | - '5030:5030' 86 | - '15030:15672' 87 | volumes: 88 | - ./infra/rabbitmq/rabbitmq.config:/etc/rabbitmq/rabbitmq.config:ro 89 | - ./infra/rabbitmq/definitions.json:/etc/rabbitmq/definitions.json:ro 90 | networks: 91 | iris: 92 | driver: bridge 93 | -------------------------------------------------------------------------------- /docs/docs/about/contact.md: -------------------------------------------------------------------------------- 1 | # Contact 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/docs/about/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/docs/about/team.md: -------------------------------------------------------------------------------- 1 | # Team 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/docs/contribution/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/docs/contribution/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/docs/contribution/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/docs/contribution/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/docs/contribution/user_interface.md: -------------------------------------------------------------------------------- 1 | # User Interface 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/docs/contribution/worker.md: -------------------------------------------------------------------------------- 1 | # Worker 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/docs/features/introduction.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuomkar/iris/df57b63e708851323ec01ca7d247f5d5b14e361e/docs/docs/features/introduction.md -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuomkar/iris/df57b63e708851323ec01ca7d247f5d5b14e361e/docs/docs/index.md -------------------------------------------------------------------------------- /docs/docs/setup/introduction.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuomkar/iris/df57b63e708851323ec01ca7d247f5d5b14e361e/docs/docs/setup/introduction.md -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: iris 2 | site_description: IRIS Documentation 3 | site_author: Omkar Prabhu 4 | site_url: https://iris-docs.readthedocs.io 5 | 6 | repo_name: prabhuomkar/iris 7 | repo_url: https://github.com/prabhuomkar/iris 8 | edit_uri: https://github.com/prabhuomkar/iris/tree/master/docs 9 | 10 | theme: 11 | name: 'material' 12 | static_templates: 13 | - 404.html 14 | include_search_page: false 15 | search_index_only: true 16 | features: 17 | - navigation.tabs 18 | - navigation.tabs.sticky 19 | - navigation.top 20 | palette: 21 | scheme: default 22 | primary: white 23 | accent: purple 24 | font: 25 | text: Roboto 26 | code: Roboto Mono 27 | 28 | nav: 29 | - Features: 30 | - Introduction: index.md 31 | - Setup: 32 | - Introduction: setup/introduction.md 33 | - Contribution: 34 | - Introduction: contribution/introduction.md 35 | - Development: contribution/development.md 36 | - Architecture: contribution/architecture.md 37 | - User Interface: contribution/user_interface.md 38 | - API: contribution/api.md 39 | - Worker: contribution/worker.md 40 | - About: 41 | - Team: about/team.md 42 | - Contact: about/contact.md 43 | - License: about/license.md 44 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs 2 | mkdocs-material -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:react/recommended"], 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "jsx": true 10 | }, 11 | "ecmaVersion": 12, 12 | "sourceType": "module" 13 | }, 14 | "plugins": ["react"], 15 | "ignorePatterns": ["*.test.js"], 16 | "rules": { 17 | "react/jsx-filename-extension": "off", 18 | "no-undef": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine as builder 2 | RUN mkdir /app 3 | WORKDIR /app 4 | COPY package.json /app 5 | RUN npm install 6 | COPY . /app 7 | ARG REACT_APP_API_URL 8 | ENV REACT_APP_API_URL $REACT_APP_API_URL 9 | RUN npm run build 10 | 11 | FROM nginx:1.21.3-alpine 12 | ENV NODE_ENV production 13 | COPY --from=builder /app/build /usr/share/nginx/html 14 | COPY nginx.conf /etc/nginx/conf.d/default.conf 15 | EXPOSE 80 16 | CMD ["nginx", "-g", "daemon off;"] 17 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # frontend 2 | 3 | ## Getting Started 4 | 5 | ### Prerequisites 6 | Following are the softwares requried to get everything up and running. 7 | - [Docker](https://docs.docker.com/engine/install/) for Infrastructure Components 8 | - [Node.js](https://nodejs.org/en/) for UI 9 | 10 | ### Installing 11 | **For Local Setup** 12 | - Add the below line in your `/etc/hosts` 13 | ``` 14 | 127.0.0.1 cdn-master cdn-volume database api queue frontend worker ml 15 | ``` 16 | - Build and start the containers 17 | ``` 18 | docker-compose up -d 19 | ``` 20 | 21 | **For Frontend** 22 | - Install the npm dependencies 23 | ``` 24 | npm install 25 | ``` 26 | - Start the frontend 27 | ``` 28 | npm start 29 | ``` 30 | - Lint the code 31 | ``` 32 | npm run lint 33 | ``` 34 | 35 | ### Configuration 36 | TBD 37 | -------------------------------------------------------------------------------- /frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | location / { 5 | root /usr/share/nginx/html/; 6 | include /etc/nginx/mime.types; 7 | try_files $uri $uri/ /index.html; 8 | } 9 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iris", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/client": "^3.4.11", 7 | "@apollo/react-hooks": "^4.0.0", 8 | "@rmwc/circular-progress": "^7.0.3", 9 | "@rmwc/dialog": "^7.0.3", 10 | "@rmwc/drawer": "^7.0.3", 11 | "@rmwc/grid": "^7.0.3", 12 | "@rmwc/icon": "^7.0.3", 13 | "@rmwc/image-list": "^7.0.3", 14 | "@rmwc/linear-progress": "^7.0.3", 15 | "@rmwc/list": "^7.0.3", 16 | "@rmwc/snackbar": "^7.0.3", 17 | "@rmwc/textfield": "^7.0.3", 18 | "@rmwc/theme": "^7.0.3", 19 | "@rmwc/top-app-bar": "^7.0.3", 20 | "@rmwc/typography": "^7.0.3", 21 | "@testing-library/jest-dom": "^5.14.1", 22 | "@testing-library/react": "^11.2.7", 23 | "@testing-library/user-event": "^12.8.3", 24 | "apollo-upload-client": "^16.0.0", 25 | "fractional": "^1.0.0", 26 | "graphql": "^15.5.3", 27 | "i": "^0.3.7", 28 | "moment": "^2.29.1", 29 | "node-sass": "^6.0.1", 30 | "npm": "^8.1.2", 31 | "pretty-bytes": "^5.6.0", 32 | "react": "^17.0.2", 33 | "react-dom": "^17.0.2", 34 | "react-router-dom": "^5.3.0", 35 | "react-scripts": "4.0.3", 36 | "web-vitals": "^1.1.2" 37 | }, 38 | "scripts": { 39 | "start": "react-scripts start", 40 | "build": "react-scripts build", 41 | "test": "react-scripts test", 42 | "eject": "react-scripts eject", 43 | "lint": "eslint .", 44 | "lint:fix": "eslint . --fix" 45 | }, 46 | "eslintConfig": { 47 | "extends": [ 48 | "react-app", 49 | "react-app/jest" 50 | ] 51 | }, 52 | "browserslist": { 53 | "production": [ 54 | ">0.2%", 55 | "not dead", 56 | "not op_mini all" 57 | ], 58 | "development": [ 59 | "last 1 chrome version", 60 | "last 1 firefox version", 61 | "last 1 safari version" 62 | ] 63 | }, 64 | "devDependencies": { 65 | "eslint": "^7.16.0", 66 | "eslint-config-standard": "^16.0.2", 67 | "eslint-plugin-import": "^2.22.1", 68 | "eslint-plugin-node": "^11.1.0", 69 | "eslint-plugin-promise": "^4.2.1", 70 | "eslint-plugin-react": "^7.21.5" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuomkar/iris/df57b63e708851323ec01ca7d247f5d5b14e361e/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/favourites.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | 15 | 16 | 20 | 21 | iris 22 | 23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/public/pytorch2021.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuomkar/iris/df57b63e708851323ec01ca7d247f5d5b14e361e/frontend/public/pytorch2021.png -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, createContext } from 'react'; 2 | import { createUploadLink } from 'apollo-upload-client'; 3 | import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; 4 | import { ThemeProvider } from '@rmwc/theme'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | import { Header, Content } from './components'; 7 | import SideNav from './components/SideNav'; 8 | import { DrawerAppContent } from '@rmwc/drawer'; 9 | import '@rmwc/theme/styles'; 10 | import './App.scss'; 11 | export const AlbumsContext = createContext(); 12 | 13 | const link = createUploadLink({ uri: process.env.REACT_APP_API_URL }); 14 | const client = new ApolloClient({ 15 | link, 16 | cache: new InMemoryCache(), 17 | }); 18 | 19 | const App = () => { 20 | const [open, setOpen] = useState(true); 21 | const toggle = () => setOpen(!open); 22 | 23 | const [imageList, setImageList] = useState([]); 24 | const [removeImageList, setRemoveImageList] = useState([]); 25 | const [addImageList, setAddImageList] = useState([]); 26 | 27 | return ( 28 | 29 |
30 | 37 | 45 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default App; 60 | -------------------------------------------------------------------------------- /frontend/src/App.scss: -------------------------------------------------------------------------------- 1 | $font: 'Inter', sans-serif !important; 2 | $primary-color: #ffffff; 3 | $secondary-color: #812ce5; 4 | $font-color: #000000; 5 | $link-color: #4800b2; 6 | $primary-light-color: #eadafb; 7 | $primary-dark-color: #4800b2; 8 | 9 | body { 10 | margin: 0; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | font-family: $font; 14 | background-color: $primary-color; 15 | color: $font-color; 16 | } 17 | 18 | body, 19 | span { 20 | font-family: $font; 21 | } 22 | 23 | strong, 24 | b { 25 | font-weight: 500; 26 | } 27 | 28 | h1, 29 | h2, 30 | h3, 31 | h4, 32 | h5 { 33 | font-weight: 500; 34 | } 35 | 36 | p { 37 | line-height: 24px; 38 | word-wrap: break-word; 39 | color: $font-color; 40 | } 41 | 42 | .link { 43 | text-decoration: none; 44 | color: $link-color; 45 | } 46 | 47 | .top-app-bar { 48 | background-color: $primary-color; 49 | border-bottom: 1px solid #e0e0e0; 50 | } 51 | 52 | .header-title { 53 | display: flex; 54 | text-decoration: none; 55 | color: #812ce5 !important; 56 | align-content: space-between; 57 | } 58 | 59 | .icon { 60 | position: relative; 61 | font-size: 30px; 62 | margin-right: 5px; 63 | top: 6px; 64 | } 65 | 66 | /* sidenav styles start */ 67 | .drawer { 68 | position: fixed; 69 | margin: 1px 0px !important; 70 | } 71 | 72 | .drawer-content { 73 | padding: 0px 10px; 74 | } 75 | 76 | .list-item { 77 | margin: 5px 0px !important; 78 | } 79 | 80 | .drawer-subtitle { 81 | font-size: 12px; 82 | font-weight: bold; 83 | margin: 5px 0px 10px 15px; 84 | text-transform: uppercase; 85 | color: #9e9e9e !important; 86 | } 87 | 88 | .side-nav-icon { 89 | color: #757575; 90 | margin-right: 16px; 91 | } 92 | 93 | .nav-link { 94 | text-decoration: none; 95 | display: block; 96 | color: black; 97 | } 98 | 99 | .nav-link:hover { 100 | background: $primary-light-color; 101 | color: $primary-dark-color !important; 102 | border-radius: 4px; 103 | } 104 | 105 | .nav-link:active { 106 | background: $primary-light-color; 107 | border-radius: 4px; 108 | } 109 | 110 | .activated { 111 | border-radius: 4px; 112 | background: $primary-light-color; 113 | } 114 | 115 | .activated > * { 116 | color: $primary-dark-color !important; 117 | font-weight: bold; 118 | } 119 | 120 | .activated .side-nav-icon { 121 | color: $primary-dark-color !important; 122 | } 123 | /* sidenav styles end */ 124 | 125 | .search-bar-section { 126 | width: 100%; 127 | } 128 | 129 | .search-bar > .mdc-text-field__input { 130 | padding-top: 16px; 131 | } 132 | 133 | .search-bar { 134 | border-radius: 4px !important; 135 | background-color: #f1f3f4 !important; 136 | height: 40px; 137 | width: 100%; 138 | } 139 | 140 | .search-bar:focus { 141 | outline: none !important; 142 | } 143 | 144 | @media only screen and (max-width: 600px) { 145 | .search-bar-section { 146 | max-width: 180px; 147 | } 148 | } 149 | 150 | .grid-cols > .mdc-layout-grid__inner { 151 | display: flex; 152 | justify-content: space-between; 153 | } 154 | 155 | .mdc-image-list__image-aspect-container .mdc-image-list__image { 156 | object-fit: cover !important; 157 | } 158 | 159 | .mdc-image-list--with-text-protection .mdc-image-list__supporting { 160 | background: rgba(0, 0, 0, 0.2) !important; 161 | } 162 | 163 | .mdc-layout-grid { 164 | padding: 18px 24px 0px 24px !important; 165 | } 166 | 167 | .mdc-layout-grid__inner { 168 | grid-gap: 12px !important; 169 | } 170 | 171 | .mdc-dialog .mdc-dialog__scrim { 172 | background: transparent; 173 | } 174 | 175 | .mdc-dialog__surface { 176 | box-shadow: 0px 0px 8px 2px #000000a1 !important; 177 | } 178 | 179 | .edit-section { 180 | display: flex; 181 | align-items: center; 182 | } 183 | 184 | .mdc-image-list__supporting { 185 | justify-content: center !important; 186 | } 187 | 188 | .album-list-info { 189 | justify-content: left !important; 190 | } 191 | 192 | .mdc-image-list__label, 193 | .mdc-text-field__input { 194 | font-family: $font; 195 | } 196 | 197 | .photo-grid-cell { 198 | position: relative; 199 | } 200 | 201 | .fav-icon { 202 | position: absolute; 203 | top: 0; 204 | right: 0; 205 | margin: 8px 8px 0px 0px; 206 | } 207 | 208 | .del-icon { 209 | position: absolute; 210 | top: 0; 211 | left: 0; 212 | margin: 8px 0px 0px 8px; 213 | } 214 | 215 | .select-photo { 216 | position: relative; 217 | 218 | .select-icon { 219 | position: absolute; 220 | top: 5px; 221 | left: 5px; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/iris/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/src/components/Content.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | import { 4 | Photo, 5 | Photos, 6 | Search, 7 | Upcoming, 8 | PageNotFound, 9 | Favourites, 10 | Trash, 11 | Albums, 12 | Album, 13 | } from '../pages'; 14 | import { Explore, People, Places, Things, Entity } from '../pages/explore'; 15 | 16 | const Content = () => { 17 | return ( 18 | <> 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {/* static */} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default Content; 46 | -------------------------------------------------------------------------------- /frontend/src/components/DeleteAction.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { gql, useMutation } from '@apollo/client'; 3 | import { useHistory } from 'react-router-dom'; 4 | import PropTypes from 'prop-types'; 5 | import { Snackbar } from '@rmwc/snackbar'; 6 | import '@rmwc/snackbar/styles'; 7 | import { Icon } from '@rmwc/icon'; 8 | 9 | const DELETE_ITEM = gql` 10 | mutation deleteItem($id: String!, $type: String!) { 11 | delete(id: $id, type: $type) 12 | } 13 | `; 14 | 15 | const DeleteAction = ({ deleted, id, deleteType }) => { 16 | const history = useHistory(); 17 | const [deleteItem, { data: delData, loading: delLoading }] = 18 | useMutation(DELETE_ITEM); 19 | 20 | const handleDelButtonClick = (photoId, action) => { 21 | deleteItem({ 22 | variables: { id: photoId, type: action }, 23 | }); 24 | }; 25 | 26 | if (delLoading) 27 | return ( 28 | <> 29 | {deleteType ? ( 30 | 31 | ) : ( 32 | 36 | )} 37 | 38 | ); 39 | 40 | if (delData && delData.delete) { 41 | setTimeout(() => { 42 | history.push('/'); 43 | }, 2000); 44 | 45 | return ( 46 | <> 47 | {deleteType ? ( 48 | 49 | ) : ( 50 | 54 | )} 55 | 56 | ); 57 | } 58 | 59 | return ( 60 | <> 61 | {deleteType === 'permanent' ? ( 62 | handleDelButtonClick(id, 'permanent')} 66 | /> 67 | ) : ( 68 | handleDelButtonClick(id, deleted ? 'remove' : 'add')} 72 | /> 73 | )} 74 | 75 | ); 76 | }; 77 | 78 | DeleteAction.propTypes = { 79 | deleted: PropTypes.bool, 80 | id: PropTypes.string, 81 | deleteType: PropTypes.string, 82 | }; 83 | 84 | export default DeleteAction; 85 | -------------------------------------------------------------------------------- /frontend/src/components/DeleteAlbumDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { useHistory } from 'react-router-dom'; 4 | import { gql, useMutation } from '@apollo/client'; 5 | import { CircularProgress } from '@rmwc/circular-progress'; 6 | import { Snackbar } from '@rmwc/snackbar'; 7 | import { Dialog, DialogTitle, DialogActions, DialogButton } from '@rmwc/dialog'; 8 | import '@rmwc/dialog/styles'; 9 | 10 | const DELETE_ALBUM = gql` 11 | mutation deleteAlbum($id: String!) { 12 | deleteAlbum(id: $id) 13 | } 14 | `; 15 | 16 | const DeleteAlbumDialog = ({ open, setOpen, albumName, albumId }) => { 17 | const history = useHistory(); 18 | const [deleteAlbum, { data: delData, loading: delLoading, error: delError }] = 19 | useMutation(DELETE_ALBUM); 20 | 21 | const handleDeleteAlbum = (albumId) => { 22 | deleteAlbum({ 23 | variables: { id: albumId }, 24 | }); 25 | }; 26 | 27 | if (delData && delData.deleteAlbum) { 28 | setTimeout(() => { 29 | history.push('/albums'); 30 | }, 2000); 31 | return ; 32 | } 33 | 34 | return ( 35 | { 38 | setOpen(false); 39 | }} 40 | > 41 | 42 | Are you sure you want to delete album "{albumName}"? 43 | 44 |
45 |
46 | 47 | {delLoading && }    48 | {delError && <>Sorry, some error occured!}    49 | handleDeleteAlbum(albumId)} 52 | style={{ color: '#fff' }} 53 | > 54 | Delete 55 | 56 |    57 | 58 | Close 59 | 60 | 61 |
62 | ); 63 | }; 64 | 65 | DeleteAlbumDialog.propTypes = { 66 | open: PropTypes.bool, 67 | setOpen: PropTypes.func, 68 | albumId: PropTypes.string, 69 | albumName: PropTypes.string, 70 | }; 71 | 72 | export default DeleteAlbumDialog; 73 | -------------------------------------------------------------------------------- /frontend/src/components/EditAlbum.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { gql, useMutation } from '@apollo/client'; 3 | import PropTypes from 'prop-types'; 4 | import { Icon } from '@rmwc/icon'; 5 | import { Button } from '@rmwc/button'; 6 | import { TextField } from '@rmwc/textfield'; 7 | import { CircularProgress } from '@rmwc/circular-progress'; 8 | 9 | const UPDATE_ALBUM = gql` 10 | mutation updateAlbum($id: String!, $name: String!) { 11 | updateAlbum(id: $id, input: { name: $name }) 12 | } 13 | `; 14 | 15 | const EditAlbum = ({ albumId, albumName }) => { 16 | const [ 17 | updateAlbum, 18 | { loading: updateAlbumNameLoading, error: updateAlbumNameyError }, 19 | ] = useMutation(UPDATE_ALBUM); 20 | 21 | const [showEdit, setShowEdit] = useState(false); 22 | const [updatedAlbumName, setupdatedAlbumName] = useState(albumName); 23 | 24 | const handleEditAlbumName = (albumId, updatedAlbumName) => { 25 | updateAlbum({ variables: { id: albumId, name: updatedAlbumName } }); 26 | setShowEdit(false); 27 | }; 28 | 29 | return ( 30 | <> 31 | {showEdit ? ( 32 | <> 33 |
34 | setupdatedAlbumName(e.target.value)} 37 | style={{ height: '36px' }} 38 | /> 39 |     40 |
59 |
60 |
61 | 62 | ) : ( 63 | <> 64 |

{updatedAlbumName}

65 |     66 | setShowEdit(!showEdit)} 68 | style={{ cursor: 'pointer', color: '#424242' }} 69 | icon={{ icon: 'edit', size: 'small' }} 70 | /> 71 | 72 | )} 73 | 74 | ); 75 | }; 76 | 77 | EditAlbum.propTypes = { 78 | albumId: PropTypes.string, 79 | albumName: PropTypes.string, 80 | }; 81 | 82 | export default EditAlbum; 83 | -------------------------------------------------------------------------------- /frontend/src/components/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid, GridCell } from '@rmwc/grid'; 3 | import '@rmwc/grid/styles'; 4 | 5 | const Error = () => { 6 | return ( 7 | <> 8 | 9 | 10 | 11 |
12 | 13 |
14 |
15 | Sorry, something went wrong. 16 |
17 |
18 | 19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Error; 25 | -------------------------------------------------------------------------------- /frontend/src/components/FavouriteAction.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { gql, useMutation } from '@apollo/client'; 3 | import PropTypes from 'prop-types'; 4 | import { Icon } from '@rmwc/icon'; 5 | 6 | const FAVOURITE = gql` 7 | mutation favourite($id: String!, $type: String!) { 8 | favourite(id: $id, type: $type) 9 | } 10 | `; 11 | 12 | const FavouriteAction = ({ liked, id }) => { 13 | const [fav, setFav] = useState(false); 14 | 15 | useEffect(() => { 16 | setFav(liked); 17 | }, [liked]); 18 | 19 | const [favourite, { loading: favLoading }] = 20 | useMutation(FAVOURITE); 21 | 22 | const handleFavButtonClick = (photoId, action) => { 23 | favourite({ 24 | variables: { id: photoId, type: action }, 25 | }); 26 | setFav(!fav); 27 | }; 28 | 29 | if (favLoading) return null; 30 | 31 | return ( 32 | handleFavButtonClick(id, fav ? 'remove' : 'add')} 36 | /> 37 | ); 38 | }; 39 | 40 | FavouriteAction.propTypes = { 41 | liked: PropTypes.bool, 42 | id: PropTypes.string, 43 | }; 44 | 45 | export default FavouriteAction; 46 | -------------------------------------------------------------------------------- /frontend/src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid, GridCell } from '@rmwc/grid'; 3 | import { CircularProgress } from '@rmwc/circular-progress'; 4 | import '@rmwc/circular-progress/styles'; 5 | import '@rmwc/grid/styles'; 6 | 7 | const Loading = () => { 8 | return ( 9 | <> 10 | 11 | 12 | 13 |
14 | 15 |
16 | Loading... 17 |
18 |
19 | 20 |
21 | 22 | ); 23 | }; 24 | 25 | export default Loading; 26 | -------------------------------------------------------------------------------- /frontend/src/components/PeopleList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import { 4 | ImageList, 5 | ImageListImage, 6 | ImageListItem, 7 | ImageListSupporting, 8 | ImageListLabel, 9 | ImageListImageAspectContainer, 10 | } from '@rmwc/image-list'; 11 | import { capEntityName } from '../utils'; 12 | 13 | const PeopleList = (data) => { 14 | let history = useHistory(); 15 | const stylePeopleList = { 16 | width: '80px', 17 | margin: '8px 6px 8px 6px', 18 | }; 19 | 20 | const getEntity = (data) => 21 | data 22 | .filter((e) => e.entityType === 'people') 23 | .reduce((prev, curr) => { 24 | prev.push(curr); 25 | return prev; 26 | }, []); 27 | 28 | return ( 29 | <> 30 | 31 | {getEntity(data.data).map((src) => ( 32 | 33 | 34 | 37 | history.push(`/explore/${src.entityType}/${src.id}`) 38 | } 39 | style={{ borderRadius: '6px', cursor: 'pointer' }} 40 | /> 41 | 42 | 43 | 49 | {capEntityName(src.name)} 50 | 51 | 52 | 53 | ))} 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default PeopleList; 60 | -------------------------------------------------------------------------------- /frontend/src/components/SideNav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { NavLink } from 'react-router-dom'; 4 | import { Drawer, DrawerSubtitle, DrawerContent } from '@rmwc/drawer'; 5 | import { List, ListItem, ListDivider } from '@rmwc/list'; 6 | import { Icon } from '@rmwc/icon'; 7 | import '@rmwc/drawer/styles'; 8 | import '@rmwc/list/styles'; 9 | 10 | const SideNav = (props) => { 11 | const { open } = props; 12 | 13 | const sideNavItems = [ 14 | { 15 | subtitle: '', 16 | items: [ 17 | { id: 'Photos', link_to: '', icon: 'image' }, 18 | { id: 'Explore', link_to: 'explore', icon: 'search' }, 19 | { id: 'Sharing', link_to: 'sharing', icon: 'people' }, 20 | ], 21 | }, 22 | 23 | { 24 | subtitle: 'LIBRARY', 25 | items: [ 26 | { id: 'Favourites', link_to: 'favourites', icon: 'star_rate' }, 27 | { id: 'Albums', link_to: 'albums', icon: 'photo_album' }, 28 | { id: 'Utilities', link_to: 'utilities', icon: 'filter_none' }, 29 | { id: 'Trash', link_to: 'trash', icon: 'delete' }, 30 | ], 31 | }, 32 | ]; 33 | 34 | return ( 35 | 36 | 37 | 38 | {sideNavItems.map((section) => ( 39 |
40 | {section.subtitle ? ( 41 | 42 | {section.subtitle} 43 | 44 | ) : ( 45 | <> 46 | )} 47 | {section.items.map((item) => ( 48 | 55 | 56 | 57 | {item.id} 58 | 59 | 60 | ))} 61 |
62 | ))} 63 | 64 |
65 |
66 |
67 | ); 68 | }; 69 | 70 | SideNav.propTypes = { 71 | open: PropTypes.bool, 72 | }; 73 | 74 | export default SideNav; 75 | -------------------------------------------------------------------------------- /frontend/src/components/UpdateAlbum.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { useHistory } from 'react-router-dom'; 4 | import { gql, useMutation } from '@apollo/client'; 5 | import { TopAppBarActionItem } from '@rmwc/top-app-bar'; 6 | import { Snackbar } from '@rmwc/snackbar'; 7 | import { Button } from '@rmwc/button'; 8 | import { Error } from '.'; 9 | 10 | const UPDATE_ALBUM = gql` 11 | mutation updateAlbumMediaItems( 12 | $id: String! 13 | $type: String! 14 | $mediaItems: [String!]! 15 | ) { 16 | updateAlbumMediaItems(id: $id, type: $type, mediaItems: $mediaItems) 17 | } 18 | `; 19 | 20 | const UpdateAlbum = ({ disabled, albumId, removeImageList, addImageList }) => { 21 | const history = useHistory(); 22 | const [updateAlbum, { data, error, loading }] = useMutation(UPDATE_ALBUM); 23 | 24 | const handleUpdateAlbum = (updatedList, type) => { 25 | updateAlbum({ 26 | variables: { 27 | id: albumId, 28 | type: type, 29 | mediaItems: updatedList, 30 | }, 31 | }); 32 | }; 33 | 34 | if (loading) 35 | return ( 36 | 44 | ); 45 | 46 | if (data && data.updateAlbumMediaItems) { 47 | setTimeout(() => { 48 | if (removeImageList) { 49 | history.go(0); 50 | } else { 51 | history.push(`/album/${albumId}`); 52 | } 53 | }); 54 | return ( 55 | 63 | ); 64 | } 65 | 66 | if (error) return ; 67 | 68 | return ( 69 | <> 70 | {removeImageList ? ( 71 | handleUpdateAlbum(removeImageList, 'remove')} 76 | /> 77 | ) : ( 78 |